[css_colors] Import from flutter/css_colors
diff --git a/.ci/Dockerfile b/.ci/Dockerfile
new file mode 100644
index 0000000..a7b75d6
--- /dev/null
+++ b/.ci/Dockerfile
@@ -0,0 +1,20 @@
+# Last updated 10/22/2020 (to rebuild the docker image, update this timestamp)
+FROM cirrusci/flutter:stable-web
+
+RUN sudo apt-get update && \
+ sudo apt-get upgrade --yes && \
+ sudo apt-get install --yes gpg-agent && \
+ sudo apt-get clean --yes
+
+# This must occur after the install of gpg-agent
+RUN 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-5.0 main" && \
+ sudo apt-get update && \
+ sudo apt-get install --yes --allow-unauthenticated clang-format-5.0 && \
+ sudo apt-get clean --yes
+
+RUN yes | sdkmanager \
+ "platforms;android-27" \
+ "build-tools;27.0.3" \
+ "extras;google;m2repository" \
+ "extras;android;m2repository"
diff --git a/.cirrus.yml b/.cirrus.yml
new file mode 100644
index 0000000..1147347
--- /dev/null
+++ b/.cirrus.yml
@@ -0,0 +1,90 @@
+gcp_credentials: ENCRYPTED[1816835da1e936dabb469b51501856ec8468676e35e967bd0fd720a815498e5ee6c8a6a79219ce273f67bcb8f1aa948a]
+
+task:
+ gke_container:
+ dockerfile: .ci/Dockerfile
+ builder_image_name: docker-builder # gce vm image
+ builder_image_project: flutter-cirrus
+ cluster_name: test-cluster
+ zone: us-central1-a
+ namespace: default
+ cpu: 4
+ memory: 8G
+ upgrade_script:
+ - flutter channel master
+ - flutter upgrade
+ - flutter doctor
+ - git fetch origin master
+ activate_script: pub global activate flutter_plugin_tools
+ matrix:
+ - name: analyze
+ script: ./script/incremental_build.sh analyze --custom-analysis=web_benchmarks/testing/test_app
+ - name: publishable
+ script: ./script/check_publish.sh
+ depends_on:
+ - analyze
+ - name: test+format
+ format_script: ./script/incremental_build.sh format --travis --clang-format=clang-format-5.0
+ test_script: ./script/incremental_build.sh test
+ depends_on:
+ - analyze
+ - name: build-apks+java-test
+ env:
+ matrix:
+ BUILD_SHARDING: "--shardIndex 0 --shardCount 2"
+ BUILD_SHARDING: "--shardIndex 1 --shardCount 2"
+ script:
+ - ./script/incremental_build.sh build-examples --apk
+ - ./script/incremental_build.sh java-test # must come after apk build
+ depends_on:
+ - analyze
+ - name: web_benchmarks_test
+ script:
+ - ./script/install_chromium.sh
+ - export CHROME_EXECUTABLE=$(pwd)/.chromium/chrome-linux/chrome
+ - flutter config --enable-web
+ - cd packages/web_benchmarks/testing/test_app
+ - flutter packages get
+ - cd ../..
+ - flutter packages get
+ - dart testing/web_benchmarks_test.dart
+
+task:
+ name: build-ipas
+ use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true'
+ osx_instance:
+ image: big-sur-xcode-12.4
+ env:
+ PATH: $PATH:/usr/local/bin
+ matrix:
+ BUILD_SHARDING: "--shardIndex 0 --shardCount 2"
+ BUILD_SHARDING: "--shardIndex 1 --shardCount 2"
+ setup_script:
+ - flutter channel master
+ - flutter upgrade
+ - flutter doctor
+ - git fetch origin master
+ - pub global activate flutter_plugin_tools
+ build_script:
+ - ./script/incremental_build.sh build-examples --ipa
+
+task:
+ name: local_tests
+ use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true'
+ osx_instance:
+ image: big-sur-xcode-12.4
+ env:
+ PATH: $PATH:/usr/local/bin
+ matrix:
+ CHANNEL: "master"
+ CHANNEL: "stable"
+ setup_script:
+ - git fetch origin master
+ - pub global activate flutter_plugin_tools
+ - brew install clang-format
+ upgrade_script:
+ - flutter channel $CHANNEL
+ - flutter upgrade
+ - flutter doctor
+ build_script:
+ - ./script/local_tests.sh
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..31fda4b
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,25 @@
+# Auto detect text files and perform LF normalization
+* text=auto
+
+# Always perform LF normalization on these files
+*.dart text
+*.gradle text
+*.html text
+*.java text
+*.json text
+*.md text
+*.py text
+*.sh text
+*.txt text
+*.xml text
+*.yaml text
+
+# Make sure that these Windows files always have CRLF line endings in checkout
+*.bat text eol=crlf
+*.ps1 text eol=crlf
+
+# Never perform LF normalization on these files
+*.ico binary
+*.jar binary
+*.png binary
+*.zip binary
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..da11475
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,31 @@
+*Replace this paragraph with a description of what this PR is changing or adding, and why.*
+
+*List which issues are fixed by this PR. You must list at least one issue.*
+
+*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
+
+## Pre-launch Checklist
+
+- [ ] The title of the PR starts with the name of the package surrounded by square brackets, e.g. `[shared_preferences]`
+- [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
+- [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities.
+- [ ] I read and followed the [Flutter Style Guide].
+- [ ] I listed at least one issue that this PR fixes in the description above.
+- [ ] I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt.
+- [ ] I 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 updated/added relevant documentation (doc comments with `///`).
+- [ ] I signed the [CLA].
+- [ ] All existing and new tests are passing.
+
+If you need help, consider asking for advice on the `#hackers-new` channel on [Discord].
+
+<!-- Links -->
+[Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
+[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
+[Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
+[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
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8199ca4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,30 @@
+.DS_Store
+.atom/
+.idea
+.packages
+.pub/
+.dart_tool/
+
+pubspec.lock
+
+Podfile.lock
+Pods/
+
+*instrumentscli*.trace
+*.cipd
+
+# Build directories are produced when building using the Flutter CLI.
+build
+
+# This file is produced as a back-up when web_benchmarks fails to parse a
+# Chrome trace.
+chrome-trace.json
+
+# Generated files on example apps
+flutter_export_environment.sh
+.flutter-plugins*
+local.properties
+**/Flutter/Generated.xcconfig
+
+generated_plugin_registrant.*
+GeneratedPluginRegistrant.*
diff --git a/.metadata b/.metadata
new file mode 100644
index 0000000..2c91cc0
--- /dev/null
+++ b/.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: 5e3e5a2a1a977c34b22f3709109fd237b5cab9c6
+ channel: master
+
+project_type: package
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..3bdb841
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,8 @@
+# Below is a list of people and organizations that have contributed
+# to the Flutter project. Names should be added to the list like so:
+#
+# Name/Organization <email address>
+
+Google Inc.
+Britannio Jarrett <britanniojarrett@gmail.com>
+Sarbagya Dhaubanjar <mail@sarbagyastha.com.np>
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..06ceccd
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,97 @@
+# Contributing to Flutter
+
+[](https://cirrus-ci.com/github/flutter/packages)
+
+_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/packages` 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:<your_name_here>/packages.git`
+- `cd packages`
+- `git remote add upstream git@github.com:flutter/packages.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 `flutter packages 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. For example:
+
+- `cd packages/palette_generator/example`
+- `flutter packages get`
+- `flutter run`
+
+## 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 <name_of_your_branch>`
+- Hack away.
+- Verify changes with [flutter_plugin_tools](https://pub.dartlang.org/packages/flutter_plugin_tools)
+
+```shell
+pub global activate flutter_plugin_tools
+pub global run flutter_plugin_tools format --plugins package_name
+pub global run flutter_plugin_tools analyze --plugins package_name
+pub global run flutter_plugin_tools test --plugins package_name
+```
+
+_If `pub` is not available, use `flutter pub` instead._
+
+- Check that the package can be published (but don't publish it until it has landed!):
+
+```shell
+cd packages/package_name; pub publish --dry-run
+```
+
+- `git commit -am "<your informative commit message>"`
+- `git push origin <name_of_your_branch>`
+
+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/packages` and click the
+ "Compare & pull request" button
+
+Please make sure all your checkins have detailed commit messages explaining the patch.
+
+Once you've gotten an LGTM from a project maintainer and once your PR has received
+the green light from all our automated testing (Travis, AppVeyor, etc), submit your
+changes to the `master` branch using one of the following methods:
+
+- Wait for one of the project maintainers to submit it for you.
+- Click the green "Merge pull request" button on the GitHub UI of your pull
+ request (requires commit access).
+
+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.
+
+We grant commit access to people who have gained our trust and demonstrated
+a commitment to Flutter.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8211a02
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014 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/README.md b/README.md
new file mode 100644
index 0000000..f0aea64
--- /dev/null
+++ b/README.md
@@ -0,0 +1,43 @@
+# Flutter Packages
+
+[](https://cirrus-ci.com/github/flutter/packages/master)
+
+This repo is a companion repo to the main [flutter repo](
+https://github.com/flutter/flutter). It contains the source code for Flutter's
+first-party packages (i.e., packages developed by the core Flutter team).
+Check the [`packages`](./packages) directory to see all packages.
+
+These packages are also available on [pub](https://pub.dartlang.org/flutter/packages).
+
+## Issues
+
+Please file any issues, bugs, or feature requests in the [main flutter
+repo](https://github.com/flutter/flutter/issues/new).
+
+## Contributing
+
+If you wish to contribute a new package to the Flutter ecosystem, please
+see the documentation for [developing packages](https://flutter.io/developing-packages/). You can store
+your package source code in any GitHub repository (the present repo is only
+intended for packages developed by the core Flutter team). Once your package
+is ready you can [publish](https://flutter.io/developing-packages/#publish)
+to the [pub repository](https://pub.dartlang.org/).
+
+If you wish to contribute a change to any of the existing packages in this repo,
+please review our [contribution guide](https://github.com/flutter/packages/blob/master/CONTRIBUTING.md),
+and send a [pull request](https://github.com/flutter/packages/pulls).
+
+## Plugins
+
+These are the available packages in this repository.
+
+| Plugin | Pub |
+|--------|-----|
+| [animations](./packages/animations/) | [](https://pub.dev/packages/animations) |
+| [extension_google_sign_in_as_googleapis_auth](./packages/extension_google_sign_in_as_googleapis_auth/) | [](https://pub.dev/packages/extension_google_sign_in_as_googleapis_auth) |
+| [fuchsia_ctl](./packages/fuchsia_ctl/) | [](https://pub.dev/packages/fuchsia_ctl) |
+| [multicast_dns](./packages/multicast_dns/) | [](https://pub.dev/packages/multicast_dns) |
+| [palette_generator](./packages/palette_generator/) | [](https://pub.dartlang.org/packages/palette_generator) |
+| [pigeon](./packages/pigeon/) | [](https://pub.dev/packages/pigeon) |
+| [pointer_interceptor](./packages/pointer_interceptor/) | [](https://pub.dev/packages/pointer_interceptor) |
+| [xdg_directories](./packages/xdg_directories/) | [](https://pub.dev/packages/xdg_directories) |
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..6671951
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,211 @@
+# 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
+#
+# This file is derived from the master file in the flutter repo, and should be
+# kept in sync with it.
+
+analyzer:
+ strong-mode:
+ 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
+ # Turned off until null-safe rollout is complete.
+ unnecessary_null_comparison: ignore
+
+linter:
+ rules:
+ # 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_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_dynamic_calls # not yet tested
+ - 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
+ - avoid_types_as_parameter_names
+ # - avoid_types_on_closure_parameters # conflicts with always_specify_types
+ - avoid_unnecessary_containers
+ - 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/dart-lang/linter/issues/1142
+ # - 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
+ - deprecated_consistency
+ # - 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
+ - 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
+ - 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
+ # - 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
+ - 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
+ - prefer_generic_function_type_aliases
+ - prefer_if_elements_to_conditional_expressions
+ - prefer_if_null_operators
+ - prefer_initializing_formals
+ - prefer_inlined_adds
+ # - prefer_int_literals # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#use-double-literals-for-double-constants
+ # - prefer_interpolation_to_compose_strings # doesn't work with raw strings, see https://github.com/dart-lang/linter/issues/2490
+ - 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
+ # - prefer_relative_imports # incompatible with sub-package imports
+ - prefer_single_quotes
+ - prefer_spread_collections
+ - prefer_typing_uninitialized_variables
+ - prefer_void_to_null
+ - provide_deprecation_message
+ - public_member_api_docs
+ - recursive_getters
+ - sized_box_for_whitespace
+ - slash_for_doc_comments
+ # - sort_child_properties_last # not yet tested
+ - sort_constructors_first
+ - sort_pub_dependencies
+ - sort_unnamed_constructors_first
+ - test_types_in_equals
+ - throw_in_finally
+ - tighten_type_of_initializing_formals
+ # - type_annotate_public_apis # subset of always_specify_types
+ - type_init_formals
+ # - unawaited_futures # too many false positives
+ - unnecessary_await_in_return
+ - 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
+ # - use_if_null_to_convert_nulls_to_bools # not yet tested
+ - use_is_even_rather_than_modulo
+ - use_key_in_widget_constructors
+ - use_late_for_private_fields_and_variables
+ # - use_named_constants # not yet yested
+ - 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
diff --git a/customer_testing.bat b/customer_testing.bat
new file mode 100644
index 0000000..28de6ee
--- /dev/null
+++ b/customer_testing.bat
@@ -0,0 +1,13 @@
+REM This file is used by
+REM https://github.com/flutter/tests/tree/master/registry/flutter_packages.test
+REM to run the tests of certain packages in this repository as a presubmit
+REM for the flutter/flutter repository.
+REM Changes to this file (and any tests in this repository) are only honored
+REM after the commit hash in the "flutter_packages.test" mentioned above has
+REM been updated.
+REM Remember to also update the Posix version (customer_testing.sh) when
+REM changing this file.
+
+CD packages/animations
+CALL flutter analyze
+CALL flutter test
diff --git a/customer_testing.sh b/customer_testing.sh
new file mode 100755
index 0000000..5e9b670
--- /dev/null
+++ b/customer_testing.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# This file is used by
+# https://github.com/flutter/tests/tree/master/registry/flutter_packages.test
+# to run the tests of certain packages in this repository as a presubmit
+# for the flutter/flutter repository.
+# Changes to this file (and any tests in this repository) are only honored
+# after the commit hash in the "flutter_packages.test" mentioned above has been
+# updated.
+# Remember to also update the Windows version (customer_testing.bat) when
+# changing this file.
+
+set -e
+
+cd packages/animations
+flutter analyze
+flutter test
diff --git a/dev/README.md b/dev/README.md
new file mode 100644
index 0000000..4690b19
--- /dev/null
+++ b/dev/README.md
@@ -0,0 +1,19 @@
+This directory contains resources that the Flutter team uses during
+the development of packages.
+
+## Luci builder file
+`try_builders.json` contains the supported luci try builders
+for packages. It follows format:
+```json
+{
+ "builders":[
+ {
+ "name":"yyy",
+ "repo":"packages",
+ "enabled":true
+ }
+ ]
+}
+```
+This file will be mainly used in [`flutter/cocoon`](https://github.com/flutter/cocoon)
+to trigger/update pre-submit luci tasks.
diff --git a/dev/try_builders.json b/dev/try_builders.json
new file mode 100644
index 0000000..6986e9e
--- /dev/null
+++ b/dev/try_builders.json
@@ -0,0 +1,9 @@
+{
+ "builders":[
+ {
+ "name":"fuchsia_ctl",
+ "repo":"packages",
+ "enabled":true
+ }
+ ]
+}
diff --git a/packages/animations/.gitignore b/packages/animations/.gitignore
new file mode 100644
index 0000000..f3c2053
--- /dev/null
+++ b/packages/animations/.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/animations/.metadata b/packages/animations/.metadata
new file mode 100644
index 0000000..28ddf84
--- /dev/null
+++ b/packages/animations/.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: ce3c467292293176452a3b4b4fee70073f5af9b3
+ channel: master
+
+project_type: package
diff --git a/packages/animations/CHANGELOG.md b/packages/animations/CHANGELOG.md
new file mode 100644
index 0000000..0ab0f79
--- /dev/null
+++ b/packages/animations/CHANGELOG.md
@@ -0,0 +1,60 @@
+## 2.0.0
+
+* Migrates to null safety.
+* Add `routeSettings` and `filter` option to `showModal`.
+
+## 1.1.2
+
+* Fixes for upcoming changes to the flutter framework.
+
+## 1.1.1
+
+* Hide implementation of `DualTransitionBuilder` as the widget has been implemented in the Flutter framework.
+
+## 1.1.0
+
+* Introduce usage of `DualTransitionBuilder` for all transition widgets, preventing ongoing animations at the start of the transition animation from resetting at the end of the transition animations.
+* Fix `FadeScaleTransition` example's `FloatingActionButton` being accessible
+and tappable when it is supposed to be hidden.
+* `showModal` now defaults to using `FadeScaleTransitionConfiguration` instead of `null`.
+* Added const constructors for `FadeScaleTransitionConfiguration` and `ModalConfiguration`.
+* Add custom fillColor property to `SharedAxisTransition` and `SharedAxisPageTransitionsBuilder`.
+* Fix prefer_const_constructors lint in test and example.
+* Add option `useRootNavigator` to `OpenContainer`.
+* Add `OpenContainer.onClosed`, which is called with a returned value when the container was popped and has returned to the closed state.
+* Fixes a bug with OpenContainer where a crash occurs when the container is dismissed after the container widget itself is removed.
+
+
+## 1.0.0+5
+
+* Fix override analyzer ignore placement.
+
+
+## 1.0.0+4
+
+* Fix a typo in the changelog dates
+* Revert use of modern Material text style nomenclature in the example app
+ to be compatible with Flutter's `stable` branch for the time being.
+* Add override analyzer ignore in modal.dart for reverseTransitionDuration
+ until Flutter's stable branch contains
+ https://github.com/flutter/flutter/pull/48274.
+
+
+## 1.0.0+3
+
+* Update README.md to better describe Material motion
+
+
+## 1.0.0+2
+
+* Fixes to pubspec.yaml
+
+
+## 1.0.0+1
+
+* Fixes to pubspec.yaml
+
+
+## 1.0.0
+
+* Initial release
diff --git a/packages/animations/LICENSE b/packages/animations/LICENSE
new file mode 100644
index 0000000..73e6b6e
--- /dev/null
+++ b/packages/animations/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2019 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/animations/README.md b/packages/animations/README.md
new file mode 100644
index 0000000..3a6f046
--- /dev/null
+++ b/packages/animations/README.md
@@ -0,0 +1,71 @@
+# High quality pre-built Animations for Flutter
+
+This package contains pre-canned animations for commonly-desired effects. The animations can be customized with your content and dropped into your application to delight your users.
+
+To see examples of the following animations on a device or simulator:
+
+```bash
+cd example/
+flutter run --release
+```
+
+## Material motion for Flutter
+
+Material motion is a set of transition patterns that help users understand and navigate an app. Currently,
+the following transition patterns are available in this library:
+
+1. [Container transform](#container-transform)
+2. [Shared axis](#shared-axis)
+3. [Fade through](#fade-through)
+4. [Fade](#fade)
+
+### Container transform
+
+The **container transform** pattern is designed for transitions between UI elements that include a container. This pattern creates a visible connection between two UI elements.
+
+
+_Examples of the container transform:_
+
+1. _A card into a details page_
+2. _A list item into a details page_
+3. _A FAB into a details page_
+4. _A search bar into expanded search_
+
+### Shared axis
+
+The **shared axis** pattern is used for transitions between UI elements that
+have a spatial or navigational relationship. This pattern uses a shared
+transformation on the x, y, or z axis to reinforce the relationship between
+elements.
+
+
+_Examples of the shared axis pattern:_
+
+1. _An onboarding flow transitions along the x-axis_
+2. _A stepper transitions along the y-axis_
+3. _A parent-child navigation transitions along the z-axis_
+
+### Fade through
+
+The **fade through** pattern is used for transitions between UI elements that do
+not have a strong relationship to each other.
+
+
+_Examples of the fade through pattern:_
+
+1. _Tapping destinations in a bottom navigation bar_
+2. _Tapping a refresh icon_
+3. _Tapping an account switcher_
+
+### Fade
+
+The **fade** pattern is used for UI elements that enter or exit within the
+bounds of the screen, such as a dialog that fades in the center of the screen.
+
+
+_Examples of the fade pattern:_
+
+1. _A dialog_
+2. _A menu_
+3. _A snackbar_
+4. _A FAB_
diff --git a/packages/animations/example/.gitignore b/packages/animations/example/.gitignore
new file mode 100644
index 0000000..ae1f183
--- /dev/null
+++ b/packages/animations/example/.gitignore
@@ -0,0 +1,37 @@
+# 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
+
+# Exceptions to above rules.
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/animations/example/.metadata b/packages/animations/example/.metadata
new file mode 100644
index 0000000..6b34df6
--- /dev/null
+++ b/packages/animations/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: 0bced151e44ffa138e608c2e2dbff3309ab8bd75
+ channel: master
+
+project_type: app
diff --git a/packages/animations/example/README.md b/packages/animations/example/README.md
new file mode 100644
index 0000000..a170f60
--- /dev/null
+++ b/packages/animations/example/README.md
@@ -0,0 +1,3 @@
+# Example Catalog for package:animations.
+
+Run `flutter run --release` in this directory to launch the catalog on a device.
diff --git a/packages/animations/example/android/.gitignore b/packages/animations/example/android/.gitignore
new file mode 100644
index 0000000..bc2100d
--- /dev/null
+++ b/packages/animations/example/android/.gitignore
@@ -0,0 +1,7 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
diff --git a/packages/animations/example/android/app/build.gradle b/packages/animations/example/android/app/build.gradle
new file mode 100644
index 0000000..5180451
--- /dev/null
+++ b/packages/animations/example/android/app/build.gradle
@@ -0,0 +1,66 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+ throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+ compileSdkVersion 28
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ lintOptions {
+ disable 'InvalidPackage'
+ }
+
+ defaultConfig {
+ applicationId "dev.flutter.packages.animations.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/animations/example/android/app/src/debug/AndroidManifest.xml b/packages/animations/example/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..7eb704d
--- /dev/null
+++ b/packages/animations/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="dev.flutter.packages.animations.example">
+ <!-- Flutter needs it to communicate with the running application
+ to allow setting breakpoints, to provide hot reload, etc.
+ -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
diff --git a/packages/animations/example/android/app/src/main/AndroidManifest.xml b/packages/animations/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1fc0ab4
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="dev.flutter.packages.animations.example">
+ <!-- io.flutter.app.FlutterApplication is an android.app.Application that
+ calls FlutterMain.startInitialization(this); in its onCreate method.
+ In most cases you can leave this as-is, but you if you want to provide
+ additional functionality it is fine to subclass or reimplement
+ FlutterApplication and put your custom class here. -->
+ <application
+ android:name="io.flutter.app.FlutterApplication"
+ android:label="example"
+ android:icon="@mipmap/ic_launcher">
+ <activity
+ android:name=".MainActivity"
+ android:launchMode="singleTop"
+ android:theme="@style/LaunchTheme"
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+ android:hardwareAccelerated="true"
+ android:windowSoftInputMode="adjustResize">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <!-- Don't delete the meta-data below.
+ This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+ <meta-data
+ android:name="flutterEmbedding"
+ android:value="2" />
+ </application>
+</manifest>
diff --git a/packages/animations/example/android/app/src/main/kotlin/dev/flutter/packages/animations/example/MainActivity.kt b/packages/animations/example/android/app/src/main/kotlin/dev/flutter/packages/animations/example/MainActivity.kt
new file mode 100644
index 0000000..cde3a3b
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/kotlin/dev/flutter/packages/animations/example/MainActivity.kt
@@ -0,0 +1,12 @@
+package dev.flutter.packages.animations.example
+
+import androidx.annotation.NonNull;
+import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugins.GeneratedPluginRegistrant
+
+class MainActivity: FlutterActivity() {
+ override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
+ GeneratedPluginRegistrant.registerWith(flutterEngine);
+ }
+}
diff --git a/packages/animations/example/android/app/src/main/res/drawable/launch_background.xml b/packages/animations/example/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@android:color/white" />
+
+ <!-- You can insert your own image assets here -->
+ <!-- <item>
+ <bitmap
+ android:gravity="center"
+ android:src="@mipmap/launch_image" />
+ </item> -->
+</layer-list>
diff --git a/packages/animations/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/animations/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/animations/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/animations/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/animations/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/animations/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/animations/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/animations/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/animations/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/animations/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/animations/example/android/app/src/main/res/values/styles.xml b/packages/animations/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..00fa441
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+ <!-- Show a splash screen on the activity. Automatically removed when
+ Flutter draws its first frame -->
+ <item name="android:windowBackground">@drawable/launch_background</item>
+ </style>
+</resources>
diff --git a/packages/animations/example/android/app/src/profile/AndroidManifest.xml b/packages/animations/example/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..7eb704d
--- /dev/null
+++ b/packages/animations/example/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="dev.flutter.packages.animations.example">
+ <!-- Flutter needs it to communicate with the running application
+ to allow setting breakpoints, to provide hot reload, etc.
+ -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
diff --git a/packages/animations/example/android/build.gradle b/packages/animations/example/android/build.gradle
new file mode 100644
index 0000000..3100ad2
--- /dev/null
+++ b/packages/animations/example/android/build.gradle
@@ -0,0 +1,31 @@
+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/animations/example/android/gradle.properties b/packages/animations/example/android/gradle.properties
new file mode 100644
index 0000000..38c8d45
--- /dev/null
+++ b/packages/animations/example/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/animations/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/animations/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..296b146
--- /dev/null
+++ b/packages/animations/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
diff --git a/packages/animations/example/android/settings.gradle b/packages/animations/example/android/settings.gradle
new file mode 100644
index 0000000..5a2f14f
--- /dev/null
+++ b/packages/animations/example/android/settings.gradle
@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+ pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+
+plugins.each { name, path ->
+ def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+ include ":$name"
+ project(":$name").projectDir = pluginDirectory
+}
diff --git a/packages/animations/example/assets/avatar_logo.png b/packages/animations/example/assets/avatar_logo.png
new file mode 100755
index 0000000..a5d394f
--- /dev/null
+++ b/packages/animations/example/assets/avatar_logo.png
Binary files differ
diff --git a/packages/animations/example/assets/placeholder_image.png b/packages/animations/example/assets/placeholder_image.png
new file mode 100644
index 0000000..913c40e
--- /dev/null
+++ b/packages/animations/example/assets/placeholder_image.png
Binary files differ
diff --git a/packages/animations/example/demo_gifs/container_transform_lineup.gif b/packages/animations/example/demo_gifs/container_transform_lineup.gif
new file mode 100644
index 0000000..c17dab1
--- /dev/null
+++ b/packages/animations/example/demo_gifs/container_transform_lineup.gif
Binary files differ
diff --git a/packages/animations/example/demo_gifs/fade_lineup.gif b/packages/animations/example/demo_gifs/fade_lineup.gif
new file mode 100644
index 0000000..76dbd18
--- /dev/null
+++ b/packages/animations/example/demo_gifs/fade_lineup.gif
Binary files differ
diff --git a/packages/animations/example/demo_gifs/fade_through_lineup.gif b/packages/animations/example/demo_gifs/fade_through_lineup.gif
new file mode 100644
index 0000000..4c5095b
--- /dev/null
+++ b/packages/animations/example/demo_gifs/fade_through_lineup.gif
Binary files differ
diff --git a/packages/animations/example/demo_gifs/shared_axis_lineup.gif b/packages/animations/example/demo_gifs/shared_axis_lineup.gif
new file mode 100644
index 0000000..2d92418
--- /dev/null
+++ b/packages/animations/example/demo_gifs/shared_axis_lineup.gif
Binary files differ
diff --git a/packages/animations/example/ios/.gitignore b/packages/animations/example/ios/.gitignore
new file mode 100644
index 0000000..e96ef60
--- /dev/null
+++ b/packages/animations/example/ios/.gitignore
@@ -0,0 +1,32 @@
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/packages/animations/example/ios/Flutter/AppFrameworkInfo.plist b/packages/animations/example/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..6b4c0f7
--- /dev/null
+++ b/packages/animations/example/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>App</string>
+ <key>CFBundleIdentifier</key>
+ <string>io.flutter.flutter.app</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>App</string>
+ <key>CFBundlePackageType</key>
+ <string>FMWK</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>MinimumOSVersion</key>
+ <string>8.0</string>
+</dict>
+</plist>
diff --git a/packages/animations/example/ios/Flutter/Debug.xcconfig b/packages/animations/example/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/packages/animations/example/ios/Flutter/Debug.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/packages/animations/example/ios/Flutter/Release.xcconfig b/packages/animations/example/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/packages/animations/example/ios/Flutter/Release.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/packages/animations/example/ios/Runner.xcodeproj/project.pbxproj b/packages/animations/example/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..0ab5cd8
--- /dev/null
+++ b/packages/animations/example/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,506 @@
+// !$*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 */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
+ 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 = "<group>"; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "<group>";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ );
+ sourceTree = "<group>";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 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 = "<group>";
+ };
+ 97C146F11CF9000F007C117D /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1020;
+ ORGANIZATIONNAME = "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\" embed_and_thin";
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 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 = "<group>";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+/* 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 = 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)";
+ 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.packages.animations.example;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 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_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)";
+ 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.packages.animations.example;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ 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.packages.animations.example;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/packages/animations/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/animations/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/animations/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "group:Runner.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/animations/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/animations/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..a28140c
--- /dev/null
+++ b/packages/animations/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "1020"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Profile"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/packages/animations/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/animations/example/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/animations/example/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "group:Runner.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/animations/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/animations/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/animations/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IDEDidComputeMac32BitWarning</key>
+ <true/>
+</dict>
+</plist>
diff --git a/packages/animations/example/ios/Runner/AppDelegate.swift b/packages/animations/example/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..70693e4
--- /dev/null
+++ b/packages/animations/example/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+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/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..dc9ada4
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..28c6bf0
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..f091b6b
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cde121
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..d0ef06e
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..dcdc230
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..c8f9ed8
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..75b2d16
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..c4df70d
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..6a84f41
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..d0e1f58
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/packages/animations/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/animations/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
+ <viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
+ </imageView>
+ </subviews>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ <constraints>
+ <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
+ <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
+ </constraints>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+ <resources>
+ <image name="LaunchImage" width="168" height="185"/>
+ </resources>
+</document>
diff --git a/packages/animations/example/ios/Runner/Base.lproj/Main.storyboard b/packages/animations/example/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ </dependencies>
+ <scenes>
+ <!--Flutter View Controller-->
+ <scene sceneID="tne-QT-ifu">
+ <objects>
+ <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+ <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+ </objects>
+ </scene>
+ </scenes>
+</document>
diff --git a/packages/animations/example/ios/Runner/Info.plist b/packages/animations/example/ios/Runner/Info.plist
new file mode 100644
index 0000000..a060db6
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>example</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>$(FLUTTER_BUILD_NAME)</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>$(FLUTTER_BUILD_NUMBER)</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UIViewControllerBasedStatusBarAppearance</key>
+ <false/>
+</dict>
+</plist>
diff --git a/packages/animations/example/ios/Runner/Runner-Bridging-Header.h b/packages/animations/example/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..7335fdf
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
\ No newline at end of file
diff --git a/packages/animations/example/lib/container_transition.dart b/packages/animations/example/lib/container_transition.dart
new file mode 100644
index 0000000..e26983f
--- /dev/null
+++ b/packages/animations/example/lib/container_transition.dart
@@ -0,0 +1,537 @@
+// 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/material.dart';
+import 'package:animations/animations.dart';
+
+const String _loremIpsumParagraph =
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod '
+ 'tempor incididunt ut labore et dolore magna aliqua. Vulputate dignissim '
+ 'suspendisse in est. Ut ornare lectus sit amet. Eget nunc lobortis mattis '
+ 'aliquam faucibus purus in. Hendrerit gravida rutrum quisque non tellus '
+ 'orci ac auctor. Mattis aliquam faucibus purus in massa. Tellus rutrum '
+ 'tellus pellentesque eu tincidunt tortor. Nunc eget lorem dolor sed. Nulla '
+ 'at volutpat diam ut venenatis tellus in metus. Tellus cras adipiscing enim '
+ 'eu turpis. Pretium fusce id velit ut tortor. Adipiscing enim eu turpis '
+ 'egestas pretium. Quis varius quam quisque id. Blandit aliquam etiam erat '
+ 'velit scelerisque. In nisl nisi scelerisque eu. Semper risus in hendrerit '
+ 'gravida rutrum quisque. Suspendisse in est ante in nibh mauris cursus '
+ 'mattis molestie. Adipiscing elit duis tristique sollicitudin nibh sit '
+ 'amet commodo nulla. Pretium viverra suspendisse potenti nullam ac tortor '
+ 'vitae.\n'
+ '\n'
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod '
+ 'tempor incididunt ut labore et dolore magna aliqua. Vulputate dignissim '
+ 'suspendisse in est. Ut ornare lectus sit amet. Eget nunc lobortis mattis '
+ 'aliquam faucibus purus in. Hendrerit gravida rutrum quisque non tellus '
+ 'orci ac auctor. Mattis aliquam faucibus purus in massa. Tellus rutrum '
+ 'tellus pellentesque eu tincidunt tortor. Nunc eget lorem dolor sed. Nulla '
+ 'at volutpat diam ut venenatis tellus in metus. Tellus cras adipiscing enim '
+ 'eu turpis. Pretium fusce id velit ut tortor. Adipiscing enim eu turpis '
+ 'egestas pretium. Quis varius quam quisque id. Blandit aliquam etiam erat '
+ 'velit scelerisque. In nisl nisi scelerisque eu. Semper risus in hendrerit '
+ 'gravida rutrum quisque. Suspendisse in est ante in nibh mauris cursus '
+ 'mattis molestie. Adipiscing elit duis tristique sollicitudin nibh sit '
+ 'amet commodo nulla. Pretium viverra suspendisse potenti nullam ac tortor '
+ 'vitae';
+
+const double _fabDimension = 56.0;
+
+/// The demo page for [OpenContainerTransform].
+class OpenContainerTransformDemo extends StatefulWidget {
+ /// Creates the demo page for [OpenContainerTransform].
+ const OpenContainerTransformDemo({Key? key}) : super(key: key);
+
+ @override
+ _OpenContainerTransformDemoState createState() {
+ return _OpenContainerTransformDemoState();
+ }
+}
+
+class _OpenContainerTransformDemoState
+ extends State<OpenContainerTransformDemo> {
+ ContainerTransitionType _transitionType = ContainerTransitionType.fade;
+
+ void _showMarkedAsDoneSnackbar(bool? isMarkedAsDone) {
+ if (isMarkedAsDone ?? false)
+ ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
+ content: Text('Marked as done!'),
+ ));
+ }
+
+ void _showSettingsBottomModalSheet(BuildContext context) {
+ showModalBottomSheet<void>(
+ context: context,
+ builder: (BuildContext context) {
+ return StatefulBuilder(
+ builder: (BuildContext context, StateSetter setModalState) {
+ return Container(
+ height: 125,
+ padding: const EdgeInsets.all(15.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: <Widget>[
+ Text(
+ 'Fade mode',
+ style: Theme.of(context).textTheme.caption,
+ ),
+ const SizedBox(height: 12),
+ ToggleButtons(
+ borderRadius: BorderRadius.circular(2.0),
+ selectedBorderColor: Theme.of(context).colorScheme.primary,
+ onPressed: (int index) {
+ setModalState(() {
+ setState(() {
+ _transitionType = index == 0
+ ? ContainerTransitionType.fade
+ : ContainerTransitionType.fadeThrough;
+ });
+ });
+ },
+ isSelected: <bool>[
+ _transitionType == ContainerTransitionType.fade,
+ _transitionType == ContainerTransitionType.fadeThrough,
+ ],
+ children: const <Widget>[
+ Text('FADE'),
+ Padding(
+ padding: EdgeInsets.symmetric(horizontal: 10.0),
+ child: Text('FADE THROUGH'),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ },
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Container transform'),
+ actions: <Widget>[
+ IconButton(
+ icon: const Icon(Icons.settings),
+ onPressed: () {
+ _showSettingsBottomModalSheet(context);
+ },
+ ),
+ ],
+ ),
+ body: ListView(
+ padding: const EdgeInsets.all(8.0),
+ children: <Widget>[
+ _OpenContainerWrapper(
+ transitionType: _transitionType,
+ closedBuilder: (BuildContext _, VoidCallback openContainer) {
+ return _ExampleCard(openContainer: openContainer);
+ },
+ onClosed: _showMarkedAsDoneSnackbar,
+ ),
+ const SizedBox(height: 16.0),
+ _OpenContainerWrapper(
+ transitionType: _transitionType,
+ closedBuilder: (BuildContext _, VoidCallback openContainer) {
+ return _ExampleSingleTile(openContainer: openContainer);
+ },
+ onClosed: _showMarkedAsDoneSnackbar,
+ ),
+ const SizedBox(height: 16.0),
+ Row(
+ children: <Widget>[
+ Expanded(
+ child: _OpenContainerWrapper(
+ transitionType: _transitionType,
+ closedBuilder: (BuildContext _, VoidCallback openContainer) {
+ return _SmallerCard(
+ openContainer: openContainer,
+ subtitle: 'Secondary text',
+ );
+ },
+ onClosed: _showMarkedAsDoneSnackbar,
+ ),
+ ),
+ const SizedBox(width: 8.0),
+ Expanded(
+ child: _OpenContainerWrapper(
+ transitionType: _transitionType,
+ closedBuilder: (BuildContext _, VoidCallback openContainer) {
+ return _SmallerCard(
+ openContainer: openContainer,
+ subtitle: 'Secondary text',
+ );
+ },
+ onClosed: _showMarkedAsDoneSnackbar,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16.0),
+ Row(
+ children: <Widget>[
+ Expanded(
+ child: _OpenContainerWrapper(
+ transitionType: _transitionType,
+ closedBuilder: (BuildContext _, VoidCallback openContainer) {
+ return _SmallerCard(
+ openContainer: openContainer,
+ subtitle: 'Secondary',
+ );
+ },
+ onClosed: _showMarkedAsDoneSnackbar,
+ ),
+ ),
+ const SizedBox(width: 8.0),
+ Expanded(
+ child: _OpenContainerWrapper(
+ transitionType: _transitionType,
+ closedBuilder: (BuildContext _, VoidCallback openContainer) {
+ return _SmallerCard(
+ openContainer: openContainer,
+ subtitle: 'Secondary',
+ );
+ },
+ onClosed: _showMarkedAsDoneSnackbar,
+ ),
+ ),
+ const SizedBox(width: 8.0),
+ Expanded(
+ child: _OpenContainerWrapper(
+ transitionType: _transitionType,
+ closedBuilder: (BuildContext _, VoidCallback openContainer) {
+ return _SmallerCard(
+ openContainer: openContainer,
+ subtitle: 'Secondary',
+ );
+ },
+ onClosed: _showMarkedAsDoneSnackbar,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16.0),
+ ...List<Widget>.generate(10, (int index) {
+ return OpenContainer<bool>(
+ transitionType: _transitionType,
+ openBuilder: (BuildContext _, VoidCallback openContainer) {
+ return const _DetailsPage();
+ },
+ onClosed: _showMarkedAsDoneSnackbar,
+ tappable: false,
+ closedShape: const RoundedRectangleBorder(),
+ closedElevation: 0.0,
+ closedBuilder: (BuildContext _, VoidCallback openContainer) {
+ return ListTile(
+ leading: Image.asset(
+ 'assets/avatar_logo.png',
+ width: 40,
+ ),
+ onTap: openContainer,
+ title: Text('List item ${index + 1}'),
+ subtitle: const Text('Secondary text'),
+ );
+ },
+ );
+ }),
+ ],
+ ),
+ floatingActionButton: OpenContainer(
+ transitionType: _transitionType,
+ openBuilder: (BuildContext context, VoidCallback _) {
+ return const _DetailsPage(
+ includeMarkAsDoneButton: false,
+ );
+ },
+ closedElevation: 6.0,
+ closedShape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(
+ Radius.circular(_fabDimension / 2),
+ ),
+ ),
+ closedColor: Theme.of(context).colorScheme.secondary,
+ closedBuilder: (BuildContext context, VoidCallback openContainer) {
+ return SizedBox(
+ height: _fabDimension,
+ width: _fabDimension,
+ child: Center(
+ child: Icon(
+ Icons.add,
+ color: Theme.of(context).colorScheme.onSecondary,
+ ),
+ ),
+ );
+ },
+ ),
+ );
+ }
+}
+
+class _OpenContainerWrapper extends StatelessWidget {
+ const _OpenContainerWrapper({
+ required this.closedBuilder,
+ required this.transitionType,
+ required this.onClosed,
+ });
+
+ final CloseContainerBuilder closedBuilder;
+ final ContainerTransitionType transitionType;
+ final ClosedCallback<bool?> onClosed;
+
+ @override
+ Widget build(BuildContext context) {
+ return OpenContainer<bool>(
+ transitionType: transitionType,
+ openBuilder: (BuildContext context, VoidCallback _) {
+ return const _DetailsPage();
+ },
+ onClosed: onClosed,
+ tappable: false,
+ closedBuilder: closedBuilder,
+ );
+ }
+}
+
+class _ExampleCard extends StatelessWidget {
+ const _ExampleCard({required this.openContainer});
+
+ final VoidCallback openContainer;
+
+ @override
+ Widget build(BuildContext context) {
+ return _InkWellOverlay(
+ openContainer: openContainer,
+ height: 300,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: <Widget>[
+ Expanded(
+ child: Container(
+ color: Colors.black38,
+ child: Center(
+ child: Image.asset(
+ 'assets/placeholder_image.png',
+ width: 100,
+ ),
+ ),
+ ),
+ ),
+ const ListTile(
+ title: Text('Title'),
+ subtitle: Text('Secondary text'),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(
+ left: 16.0,
+ right: 16.0,
+ bottom: 16.0,
+ ),
+ child: Text(
+ 'Lorem ipsum dolor sit amet, consectetur '
+ 'adipiscing elit, sed do eiusmod tempor.',
+ style: Theme.of(context)
+ .textTheme
+ .bodyText2!
+ .copyWith(color: Colors.black54),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _SmallerCard extends StatelessWidget {
+ const _SmallerCard({
+ required this.openContainer,
+ required this.subtitle,
+ });
+
+ final VoidCallback openContainer;
+ final String subtitle;
+
+ @override
+ Widget build(BuildContext context) {
+ return _InkWellOverlay(
+ openContainer: openContainer,
+ height: 225,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: <Widget>[
+ Container(
+ color: Colors.black38,
+ height: 150,
+ child: Center(
+ child: Image.asset(
+ 'assets/placeholder_image.png',
+ width: 80,
+ ),
+ ),
+ ),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.all(10.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: <Widget>[
+ Text(
+ 'Title',
+ style: Theme.of(context).textTheme.headline6,
+ ),
+ const SizedBox(height: 4),
+ Text(
+ subtitle,
+ style: Theme.of(context).textTheme.caption,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _ExampleSingleTile extends StatelessWidget {
+ const _ExampleSingleTile({required this.openContainer});
+
+ final VoidCallback openContainer;
+
+ @override
+ Widget build(BuildContext context) {
+ const double height = 100.0;
+
+ return _InkWellOverlay(
+ openContainer: openContainer,
+ height: height,
+ child: Row(
+ children: <Widget>[
+ Container(
+ color: Colors.black38,
+ height: height,
+ width: height,
+ child: Center(
+ child: Image.asset(
+ 'assets/placeholder_image.png',
+ width: 60,
+ ),
+ ),
+ ),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: <Widget>[
+ Text(
+ 'Title',
+ style: Theme.of(context).textTheme.subtitle1,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'Lorem ipsum dolor sit amet, consectetur '
+ 'adipiscing elit,',
+ style: Theme.of(context).textTheme.caption),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _InkWellOverlay extends StatelessWidget {
+ const _InkWellOverlay({
+ this.openContainer,
+ this.width,
+ this.height,
+ this.child,
+ });
+
+ final VoidCallback? openContainer;
+ final double? width;
+ final double? height;
+ final Widget? child;
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ height: height,
+ width: width,
+ child: InkWell(
+ onTap: openContainer,
+ child: child,
+ ),
+ );
+ }
+}
+
+class _DetailsPage extends StatelessWidget {
+ const _DetailsPage({this.includeMarkAsDoneButton = true});
+
+ final bool includeMarkAsDoneButton;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Details page'),
+ actions: <Widget>[
+ if (includeMarkAsDoneButton)
+ IconButton(
+ icon: const Icon(Icons.done),
+ onPressed: () => Navigator.pop(context, true),
+ tooltip: 'Mark as done',
+ )
+ ],
+ ),
+ body: ListView(
+ children: <Widget>[
+ Container(
+ color: Colors.black38,
+ height: 250,
+ child: Padding(
+ padding: const EdgeInsets.all(70.0),
+ child: Image.asset(
+ 'assets/placeholder_image.png',
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: <Widget>[
+ Text(
+ 'Title',
+ style: Theme.of(context).textTheme.headline5!.copyWith(
+ color: Colors.black54,
+ fontSize: 30.0,
+ ),
+ ),
+ const SizedBox(height: 10),
+ Text(
+ _loremIpsumParagraph,
+ style: Theme.of(context).textTheme.bodyText2!.copyWith(
+ color: Colors.black54,
+ height: 1.5,
+ fontSize: 16.0,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/packages/animations/example/lib/fade_scale_transition.dart b/packages/animations/example/lib/fade_scale_transition.dart
new file mode 100644
index 0000000..08ec008
--- /dev/null
+++ b/packages/animations/example/lib/fade_scale_transition.dart
@@ -0,0 +1,139 @@
+// 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/material.dart';
+import 'package:animations/animations.dart';
+
+/// The demo page for [FadeScaleTransition].
+class FadeScaleTransitionDemo extends StatefulWidget {
+ /// Creates the demo page for [FadeScaleTransition].
+ const FadeScaleTransitionDemo({Key? key}) : super(key: key);
+
+ @override
+ _FadeScaleTransitionDemoState createState() =>
+ _FadeScaleTransitionDemoState();
+}
+
+class _FadeScaleTransitionDemoState extends State<FadeScaleTransitionDemo>
+ with SingleTickerProviderStateMixin {
+ late AnimationController _controller;
+
+ @override
+ void initState() {
+ _controller = AnimationController(
+ value: 0.0,
+ duration: const Duration(milliseconds: 150),
+ reverseDuration: const Duration(milliseconds: 75),
+ vsync: this,
+ )..addStatusListener((AnimationStatus status) {
+ setState(() {
+ // setState needs to be called to trigger a rebuild because
+ // the 'HIDE FAB'/'SHOW FAB' button needs to be updated based
+ // the latest value of [_controller.status].
+ });
+ });
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ bool get _isAnimationRunningForwardsOrComplete {
+ switch (_controller.status) {
+ case AnimationStatus.forward:
+ case AnimationStatus.completed:
+ return true;
+ case AnimationStatus.reverse:
+ case AnimationStatus.dismissed:
+ return false;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('Fade')),
+ floatingActionButton: AnimatedBuilder(
+ animation: _controller,
+ builder: (BuildContext context, Widget? child) {
+ return FadeScaleTransition(
+ animation: _controller,
+ child: child,
+ );
+ },
+ child: Visibility(
+ visible: _controller.status != AnimationStatus.dismissed,
+ child: FloatingActionButton(
+ child: const Icon(Icons.add),
+ onPressed: () {},
+ ),
+ ),
+ ),
+ bottomNavigationBar: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: <Widget>[
+ const Divider(height: 0.0),
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ ElevatedButton(
+ onPressed: () {
+ showModal<void>(
+ context: context,
+ builder: (BuildContext context) {
+ return _ExampleAlertDialog();
+ },
+ );
+ },
+ child: const Text('SHOW MODAL'),
+ ),
+ const SizedBox(width: 10),
+ ElevatedButton(
+ onPressed: () {
+ if (_isAnimationRunningForwardsOrComplete) {
+ _controller.reverse();
+ } else {
+ _controller.forward();
+ }
+ },
+ child: _isAnimationRunningForwardsOrComplete
+ ? const Text('HIDE FAB')
+ : const Text('SHOW FAB'),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _ExampleAlertDialog extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ content: const Text('Alert Dialog'),
+ actions: <Widget>[
+ TextButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ child: const Text('CANCEL'),
+ ),
+ TextButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ child: const Text('DISCARD'),
+ ),
+ ],
+ );
+ }
+}
diff --git a/packages/animations/example/lib/fade_through_transition.dart b/packages/animations/example/lib/fade_through_transition.dart
new file mode 100644
index 0000000..65ccf49
--- /dev/null
+++ b/packages/animations/example/lib/fade_through_transition.dart
@@ -0,0 +1,187 @@
+// 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/material.dart';
+import 'package:animations/animations.dart';
+
+/// The demo page for [FadeThroughTransition].
+class FadeThroughTransitionDemo extends StatefulWidget {
+ /// Creates the demo page for [FadeThroughTransition].
+ const FadeThroughTransitionDemo({Key? key}) : super(key: key);
+
+ @override
+ _FadeThroughTransitionDemoState createState() =>
+ _FadeThroughTransitionDemoState();
+}
+
+class _FadeThroughTransitionDemoState extends State<FadeThroughTransitionDemo> {
+ int pageIndex = 0;
+
+ List<Widget> pageList = <Widget>[
+ _FirstPage(),
+ _SecondPage(),
+ _ThirdPage(),
+ ];
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('Fade through')),
+ body: PageTransitionSwitcher(
+ transitionBuilder: (
+ Widget child,
+ Animation<double> animation,
+ Animation<double> secondaryAnimation,
+ ) {
+ return FadeThroughTransition(
+ animation: animation,
+ secondaryAnimation: secondaryAnimation,
+ child: child,
+ );
+ },
+ child: pageList[pageIndex],
+ ),
+ bottomNavigationBar: BottomNavigationBar(
+ currentIndex: pageIndex,
+ onTap: (int newValue) {
+ setState(() {
+ pageIndex = newValue;
+ });
+ },
+ items: const <BottomNavigationBarItem>[
+ BottomNavigationBarItem(
+ icon: Icon(Icons.photo_library),
+ label: 'Albums',
+ ),
+ BottomNavigationBarItem(
+ icon: Icon(Icons.photo),
+ label: 'Photos',
+ ),
+ BottomNavigationBarItem(
+ icon: Icon(Icons.search),
+ label: 'Search',
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _ExampleCard extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return Expanded(
+ child: Card(
+ child: Stack(
+ children: <Widget>[
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: <Widget>[
+ Expanded(
+ child: Container(
+ color: Colors.black26,
+ child: Padding(
+ padding: const EdgeInsets.all(30.0),
+ child: Ink.image(
+ image: const AssetImage('assets/placeholder_image.png'),
+ ),
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: <Widget>[
+ Text(
+ '123 photos',
+ style: Theme.of(context).textTheme.bodyText1,
+ ),
+ Text(
+ '123 photos',
+ style: Theme.of(context).textTheme.caption,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ InkWell(
+ splashColor: Colors.black38,
+ onTap: () {},
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _FirstPage extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: <Widget>[
+ Expanded(
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: <Widget>[
+ _ExampleCard(),
+ _ExampleCard(),
+ ],
+ ),
+ ),
+ Expanded(
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: <Widget>[
+ _ExampleCard(),
+ _ExampleCard(),
+ ],
+ ),
+ ),
+ Expanded(
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: <Widget>[
+ _ExampleCard(),
+ _ExampleCard(),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+class _SecondPage extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: <Widget>[
+ _ExampleCard(),
+ _ExampleCard(),
+ ],
+ );
+ }
+}
+
+class _ThirdPage extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return ListView.builder(
+ itemBuilder: (BuildContext context, int index) {
+ return ListTile(
+ leading: Image.asset(
+ 'assets/avatar_logo.png',
+ width: 40,
+ ),
+ title: Text('List item ${index + 1}'),
+ subtitle: const Text('Secondary text'),
+ );
+ },
+ itemCount: 10,
+ );
+ }
+}
diff --git a/packages/animations/example/lib/main.dart b/packages/animations/example/lib/main.dart
new file mode 100644
index 0000000..de137e3
--- /dev/null
+++ b/packages/animations/example/lib/main.dart
@@ -0,0 +1,162 @@
+// 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/material.dart';
+import 'package:flutter/scheduler.dart';
+
+import 'container_transition.dart';
+import 'fade_scale_transition.dart';
+import 'fade_through_transition.dart';
+import 'shared_axis_transition.dart';
+
+void main() {
+ runApp(
+ MaterialApp(
+ theme: ThemeData.from(
+ colorScheme: const ColorScheme.light(),
+ ).copyWith(
+ pageTransitionsTheme: const PageTransitionsTheme(
+ builders: <TargetPlatform, PageTransitionsBuilder>{
+ TargetPlatform.android: ZoomPageTransitionsBuilder(),
+ },
+ ),
+ ),
+ home: _TransitionsHomePage(),
+ ),
+ );
+}
+
+class _TransitionsHomePage extends StatefulWidget {
+ @override
+ _TransitionsHomePageState createState() => _TransitionsHomePageState();
+}
+
+class _TransitionsHomePageState extends State<_TransitionsHomePage> {
+ bool _slowAnimations = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('Material Transitions')),
+ body: Column(
+ children: <Widget>[
+ Expanded(
+ child: ListView(
+ children: <Widget>[
+ _TransitionListTile(
+ title: 'Container transform',
+ subtitle: 'OpenContainer',
+ onTap: () {
+ Navigator.of(context).push(
+ MaterialPageRoute<void>(
+ builder: (BuildContext context) {
+ return const OpenContainerTransformDemo();
+ },
+ ),
+ );
+ },
+ ),
+ _TransitionListTile(
+ title: 'Shared axis',
+ subtitle: 'SharedAxisTransition',
+ onTap: () {
+ Navigator.of(context).push(
+ MaterialPageRoute<void>(
+ builder: (BuildContext context) {
+ return const SharedAxisTransitionDemo();
+ },
+ ),
+ );
+ },
+ ),
+ _TransitionListTile(
+ title: 'Fade through',
+ subtitle: 'FadeThroughTransition',
+ onTap: () {
+ Navigator.of(context).push(
+ MaterialPageRoute<void>(
+ builder: (BuildContext context) {
+ return const FadeThroughTransitionDemo();
+ },
+ ),
+ );
+ },
+ ),
+ _TransitionListTile(
+ title: 'Fade',
+ subtitle: 'FadeScaleTransition',
+ onTap: () {
+ Navigator.of(context).push(
+ MaterialPageRoute<void>(
+ builder: (BuildContext context) {
+ return const FadeScaleTransitionDemo();
+ },
+ ),
+ );
+ },
+ ),
+ ],
+ ),
+ ),
+ const Divider(height: 0.0),
+ SafeArea(
+ child: SwitchListTile(
+ value: _slowAnimations,
+ onChanged: (bool value) async {
+ setState(() {
+ _slowAnimations = value;
+ });
+ // Wait until the Switch is done animating before actually slowing
+ // down time.
+ if (_slowAnimations) {
+ await Future<void>.delayed(const Duration(milliseconds: 300));
+ }
+ timeDilation = _slowAnimations ? 20.0 : 1.0;
+ },
+ title: const Text('Slow animations'),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _TransitionListTile extends StatelessWidget {
+ const _TransitionListTile({
+ this.onTap,
+ required this.title,
+ required this.subtitle,
+ });
+
+ final GestureTapCallback? onTap;
+ final String title;
+ final String subtitle;
+
+ @override
+ Widget build(BuildContext context) {
+ return ListTile(
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 15.0,
+ ),
+ leading: Container(
+ width: 40.0,
+ height: 40.0,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(20.0),
+ border: Border.all(
+ color: Colors.black54,
+ ),
+ ),
+ child: const Icon(
+ Icons.play_arrow,
+ size: 35,
+ ),
+ ),
+ onTap: onTap,
+ title: Text(title),
+ subtitle: Text(subtitle),
+ );
+ }
+}
diff --git a/packages/animations/example/lib/shared_axis_transition.dart b/packages/animations/example/lib/shared_axis_transition.dart
new file mode 100644
index 0000000..707e2fd
--- /dev/null
+++ b/packages/animations/example/lib/shared_axis_transition.dart
@@ -0,0 +1,250 @@
+// 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/material.dart';
+import 'package:animations/animations.dart';
+
+/// The demo page for [SharedAxisPageTransitionsBuilder].
+class SharedAxisTransitionDemo extends StatefulWidget {
+ /// Creates the demo page for [SharedAxisPageTransitionsBuilder].
+ const SharedAxisTransitionDemo({Key? key}) : super(key: key);
+
+ @override
+ _SharedAxisTransitionDemoState createState() {
+ return _SharedAxisTransitionDemoState();
+ }
+}
+
+class _SharedAxisTransitionDemoState extends State<SharedAxisTransitionDemo> {
+ SharedAxisTransitionType? _transitionType =
+ SharedAxisTransitionType.horizontal;
+ bool _isLoggedIn = false;
+
+ void _updateTransitionType(SharedAxisTransitionType? newType) {
+ setState(() {
+ _transitionType = newType;
+ });
+ }
+
+ void _toggleLoginStatus() {
+ setState(() {
+ _isLoggedIn = !_isLoggedIn;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ resizeToAvoidBottomInset: false,
+ appBar: AppBar(title: const Text('Shared axis')),
+ body: SafeArea(
+ child: Column(
+ children: <Widget>[
+ Expanded(
+ child: PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 300),
+ reverse: !_isLoggedIn,
+ transitionBuilder: (
+ Widget child,
+ Animation<double> animation,
+ Animation<double> secondaryAnimation,
+ ) {
+ return SharedAxisTransition(
+ child: child,
+ animation: animation,
+ secondaryAnimation: secondaryAnimation,
+ transitionType: _transitionType!,
+ );
+ },
+ child: _isLoggedIn ? _CoursePage() : _SignInPage(),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 15.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: <Widget>[
+ TextButton(
+ onPressed: _isLoggedIn ? _toggleLoginStatus : null,
+ child: const Text('BACK'),
+ ),
+ ElevatedButton(
+ onPressed: _isLoggedIn ? null : _toggleLoginStatus,
+ child: const Text('NEXT'),
+ ),
+ ],
+ ),
+ ),
+ const Divider(thickness: 2.0),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ Radio<SharedAxisTransitionType>(
+ value: SharedAxisTransitionType.horizontal,
+ groupValue: _transitionType,
+ onChanged: (SharedAxisTransitionType? newValue) {
+ _updateTransitionType(newValue);
+ },
+ ),
+ const Text('X'),
+ Radio<SharedAxisTransitionType>(
+ value: SharedAxisTransitionType.vertical,
+ groupValue: _transitionType,
+ onChanged: (SharedAxisTransitionType? newValue) {
+ _updateTransitionType(newValue);
+ },
+ ),
+ const Text('Y'),
+ Radio<SharedAxisTransitionType>(
+ value: SharedAxisTransitionType.scaled,
+ groupValue: _transitionType,
+ onChanged: (SharedAxisTransitionType? newValue) {
+ _updateTransitionType(newValue);
+ },
+ ),
+ const Text('Z'),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _CoursePage extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return ListView(
+ children: <Widget>[
+ const Padding(padding: EdgeInsets.symmetric(vertical: 8.0)),
+ Text(
+ 'Streamling your courses',
+ style: Theme.of(context).textTheme.headline5,
+ textAlign: TextAlign.center,
+ ),
+ const Padding(padding: EdgeInsets.symmetric(vertical: 5.0)),
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 10.0),
+ child: Text(
+ 'Bundled categories appear as groups in your feed. '
+ 'You can always change this later.',
+ style: TextStyle(
+ fontSize: 12.0,
+ color: Colors.grey,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ const _CourseSwitch(course: 'Arts & Crafts'),
+ const _CourseSwitch(course: 'Business'),
+ const _CourseSwitch(course: 'Illustration'),
+ const _CourseSwitch(course: 'Design'),
+ const _CourseSwitch(course: 'Culinary'),
+ ],
+ );
+ }
+}
+
+class _CourseSwitch extends StatefulWidget {
+ const _CourseSwitch({
+ required this.course,
+ });
+
+ final String course;
+
+ @override
+ _CourseSwitchState createState() => _CourseSwitchState();
+}
+
+class _CourseSwitchState extends State<_CourseSwitch> {
+ bool _value = true;
+
+ @override
+ Widget build(BuildContext context) {
+ final String subtitle = _value ? 'Bundled' : 'Shown Individually';
+ return SwitchListTile(
+ title: Text(widget.course),
+ subtitle: Text(subtitle),
+ value: _value,
+ onChanged: (bool newValue) {
+ setState(() {
+ _value = newValue;
+ });
+ },
+ );
+ }
+}
+
+class _SignInPage extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return LayoutBuilder(
+ builder: (BuildContext context, BoxConstraints constraints) {
+ final double maxHeight = constraints.maxHeight;
+ return Column(
+ children: <Widget>[
+ Padding(padding: EdgeInsets.symmetric(vertical: maxHeight / 20)),
+ Image.asset(
+ 'assets/avatar_logo.png',
+ width: 80,
+ ),
+ Padding(padding: EdgeInsets.symmetric(vertical: maxHeight / 50)),
+ Text(
+ 'Hi David Park',
+ style: Theme.of(context).textTheme.headline5,
+ ),
+ Padding(padding: EdgeInsets.symmetric(vertical: maxHeight / 50)),
+ const Text(
+ 'Sign in with your account',
+ style: TextStyle(
+ fontSize: 12.0,
+ color: Colors.grey,
+ ),
+ ),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: <Widget>[
+ const Padding(
+ padding: EdgeInsets.only(
+ top: 40.0,
+ left: 15.0,
+ right: 15.0,
+ bottom: 10.0,
+ ),
+ child: TextField(
+ decoration: InputDecoration(
+ suffixIcon: Icon(
+ Icons.visibility,
+ size: 20,
+ color: Colors.black54,
+ ),
+ isDense: true,
+ labelText: 'Email or phone number',
+ border: OutlineInputBorder(),
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(left: 10.0),
+ child: TextButton(
+ onPressed: () {},
+ child: const Text('FORGOT EMAIL?'),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(left: 10.0),
+ child: TextButton(
+ onPressed: () {},
+ child: const Text('CREATE ACCOUNT'),
+ ),
+ ),
+ ],
+ ),
+ ],
+ );
+ },
+ );
+ }
+}
diff --git a/packages/animations/example/pubspec.yaml b/packages/animations/example/pubspec.yaml
new file mode 100644
index 0000000..ecee1e8
--- /dev/null
+++ b/packages/animations/example/pubspec.yaml
@@ -0,0 +1,26 @@
+name: animations_example
+description: A catalog containing example animations from package:animations.
+
+publish_to: none
+
+version: 0.0.1
+
+environment:
+ sdk: ">=2.12.0-259.9.beta <3.0.0"
+
+dependencies:
+ animations:
+ path: ../../animations
+ cupertino_icons: ^1.0.2
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+flutter:
+ uses-material-design: true
+ assets:
+ - assets/avatar_logo.png
+ - assets/placeholder_image.png
diff --git a/packages/animations/example/web/index.html b/packages/animations/example/web/index.html
new file mode 100644
index 0000000..6eff9a7
--- /dev/null
+++ b/packages/animations/example/web/index.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>example</title>
+</head>
+<body>
+ <script src="main.dart.js" type="application/javascript"></script>
+</body>
+</html>
diff --git a/packages/animations/lib/animations.dart b/packages/animations/lib/animations.dart
new file mode 100644
index 0000000..4178f80
--- /dev/null
+++ b/packages/animations/lib/animations.dart
@@ -0,0 +1,10 @@
+// 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.
+
+export 'src/fade_scale_transition.dart';
+export 'src/fade_through_transition.dart';
+export 'src/modal.dart';
+export 'src/open_container.dart';
+export 'src/page_transition_switcher.dart';
+export 'src/shared_axis_transition.dart';
diff --git a/packages/animations/lib/src/fade_scale_transition.dart b/packages/animations/lib/src/fade_scale_transition.dart
new file mode 100644
index 0000000..059e5cb
--- /dev/null
+++ b/packages/animations/lib/src/fade_scale_transition.dart
@@ -0,0 +1,180 @@
+// 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/material.dart';
+import 'package:flutter/widgets.dart';
+
+import 'modal.dart';
+
+/// The modal transition configuration for a Material fade transition.
+///
+/// The fade pattern is used for UI elements that enter or exit from within
+/// the screen bounds. Elements that enter use a quick fade in and scale from
+/// 80% to 100%. Elements that exit simply fade out. The scale animation is
+/// only applied to entering elements to emphasize new content over old.
+///
+/// ```dart
+/// /// Sample widget that uses [showModal] with [FadeScaleTransitionConfiguration].
+/// class MyHomePage extends StatelessWidget {
+/// @override
+/// Widget build(BuildContext context) {
+/// return Scaffold(
+/// body: Center(
+/// child: ElevatedButton(
+/// onPressed: () {
+/// showModal(
+/// context: context,
+/// configuration: FadeScaleTransitionConfiguration(),
+/// builder: (BuildContext context) {
+/// return _CenteredFlutterLogo();
+/// },
+/// );
+/// },
+/// child: Icon(Icons.add),
+/// ),
+/// ),
+/// );
+/// }
+/// }
+///
+/// /// Displays a centered Flutter logo with size constraints.
+/// class _CenteredFlutterLogo extends StatelessWidget {
+/// const _CenteredFlutterLogo();
+///
+/// @override
+/// Widget build(BuildContext context) {
+/// return Center(
+/// child: SizedBox(
+/// width: 250,
+/// height: 250,
+/// child: const Material(
+/// child: Center(
+/// child: FlutterLogo(size: 250),
+/// ),
+/// ),
+/// ),
+/// );
+/// }
+/// }
+/// ```
+class FadeScaleTransitionConfiguration extends ModalConfiguration {
+ /// Creates the Material fade transition configuration.
+ ///
+ /// [barrierDismissible] configures whether or not tapping the modal's
+ /// scrim dismisses the modal. [barrierLabel] sets the semantic label for
+ /// a dismissible barrier. [barrierDismissible] cannot be null. If
+ /// [barrierDismissible] is true, the [barrierLabel] cannot be null.
+ const FadeScaleTransitionConfiguration({
+ Color barrierColor = Colors.black54,
+ bool barrierDismissible = true,
+ Duration transitionDuration = const Duration(milliseconds: 150),
+ Duration reverseTransitionDuration = const Duration(milliseconds: 75),
+ String barrierLabel = 'Dismiss',
+ }) : super(
+ barrierColor: barrierColor,
+ barrierDismissible: barrierDismissible,
+ barrierLabel: barrierLabel,
+ transitionDuration: transitionDuration,
+ reverseTransitionDuration: reverseTransitionDuration,
+ );
+
+ @override
+ Widget transitionBuilder(
+ BuildContext context,
+ Animation<double> animation,
+ Animation<double> secondaryAnimation,
+ Widget child,
+ ) {
+ return FadeScaleTransition(
+ animation: animation,
+ child: child,
+ );
+ }
+}
+
+/// A widget that implements the Material fade transition.
+///
+/// The fade pattern is used for UI elements that enter or exit from within
+/// the screen bounds. Elements that enter use a quick fade in and scale from
+/// 80% to 100%. Elements that exit simply fade out. The scale animation is
+/// only applied to entering elements to emphasize new content over old.
+///
+/// This widget is not to be confused with Flutter's [FadeTransition] widget,
+/// which animates only the opacity of its child widget.
+class FadeScaleTransition extends StatelessWidget {
+ /// Creates a widget that implements the Material fade transition.
+ ///
+ /// The fade pattern is used for UI elements that enter or exit from within
+ /// the screen bounds. Elements that enter use a quick fade in and scale from
+ /// 80% to 100%. Elements that exit simply fade out. The scale animation is
+ /// only applied to entering elements to emphasize new content over old.
+ ///
+ /// This widget is not to be confused with Flutter's [FadeTransition] widget,
+ /// which animates only the opacity of its child widget.
+ ///
+ /// [animation] is typically an [AnimationController] that drives the transition
+ /// animation. [animation] cannot be null.
+ const FadeScaleTransition({
+ Key? key,
+ required this.animation,
+ this.child,
+ }) : super(key: key);
+
+ /// The animation that drives the [child]'s entrance and exit.
+ ///
+ /// See also:
+ ///
+ /// * [TransitionRoute.animate], which is the value given to this property
+ /// when it is used as a page transition.
+ final Animation<double> animation;
+
+ /// The widget below this widget in the tree.
+ ///
+ /// This widget will transition in and out as driven by [animation] and
+ /// [secondaryAnimation].
+ final Widget? child;
+
+ static final Animatable<double> _fadeInTransition = CurveTween(
+ curve: const Interval(0.0, 0.3),
+ );
+ static final Animatable<double> _scaleInTransition = Tween<double>(
+ begin: 0.80,
+ end: 1.00,
+ ).chain(CurveTween(curve: decelerateEasing));
+ static final Animatable<double> _fadeOutTransition = Tween<double>(
+ begin: 1.0,
+ end: 0.0,
+ );
+
+ @override
+ Widget build(BuildContext context) {
+ return DualTransitionBuilder(
+ animation: animation,
+ forwardBuilder: (
+ BuildContext context,
+ Animation<double> animation,
+ Widget? child,
+ ) {
+ return FadeTransition(
+ opacity: _fadeInTransition.animate(animation),
+ child: ScaleTransition(
+ scale: _scaleInTransition.animate(animation),
+ child: child,
+ ),
+ );
+ },
+ reverseBuilder: (
+ BuildContext context,
+ Animation<double> animation,
+ Widget? child,
+ ) {
+ return FadeTransition(
+ opacity: _fadeOutTransition.animate(animation),
+ child: child,
+ );
+ },
+ child: child,
+ );
+ }
+}
diff --git a/packages/animations/lib/src/fade_through_transition.dart b/packages/animations/lib/src/fade_through_transition.dart
new file mode 100644
index 0000000..8e70fc6
--- /dev/null
+++ b/packages/animations/lib/src/fade_through_transition.dart
@@ -0,0 +1,333 @@
+// 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/material.dart';
+import 'package:flutter/widgets.dart';
+
+/// Used by [PageTransitionsTheme] to define a page route transition animation
+/// in which the outgoing page fades out, then the incoming page fades in and
+/// scale up.
+///
+/// This pattern is recommended for a transition between UI elements that do not
+/// have a strong relationship to one another.
+///
+/// Scale is only applied to incoming elements to emphasize new content over
+/// old.
+///
+/// The following example shows how the FadeThroughPageTransitionsBuilder can
+/// be used in a [PageTransitionsTheme] to change the default transitions
+/// of [MaterialPageRoute]s.
+///
+/// ```dart
+/// MaterialApp(
+/// theme: ThemeData(
+/// pageTransitionsTheme: PageTransitionsTheme(
+/// builders: {
+/// TargetPlatform.android: FadeThroughPageTransitionsBuilder(),
+/// TargetPlatform.iOS: FadeThroughPageTransitionsBuilder(),
+/// },
+/// ),
+/// ),
+/// routes: {
+/// '/': (BuildContext context) {
+/// return Container(
+/// color: Colors.red,
+/// child: Center(
+/// child: TextButton(
+/// child: Text('Push route'),
+/// onPressed: () {
+/// Navigator.of(context).pushNamed('/a');
+/// },
+/// ),
+/// ),
+/// );
+/// },
+/// '/a' : (BuildContext context) {
+/// return Container(
+/// color: Colors.blue,
+/// child: Center(
+/// child: TextButton(
+/// child: Text('Pop route'),
+/// onPressed: () {
+/// Navigator.of(context).pop();
+/// },
+/// ),
+/// ),
+/// );
+/// },
+/// },
+/// );
+/// ```
+class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder {
+ /// Creates a [FadeThroughPageTransitionsBuilder].
+ const FadeThroughPageTransitionsBuilder({this.fillColor});
+
+ /// The color to use for the background color during the transition.
+ ///
+ /// This defaults to the [Theme]'s [ThemeData.canvasColor].
+ final Color? fillColor;
+
+ @override
+ Widget buildTransitions<T>(
+ PageRoute<T>? route,
+ BuildContext? context,
+ Animation<double> animation,
+ Animation<double> secondaryAnimation,
+ Widget child,
+ ) {
+ return FadeThroughTransition(
+ animation: animation,
+ secondaryAnimation: secondaryAnimation,
+ fillColor: fillColor,
+ child: child,
+ );
+ }
+}
+
+/// Defines a transition in which outgoing elements fade out, then incoming
+/// elements fade in and scale up.
+///
+/// The fade through pattern provides a transition animation between UI elements
+/// that do not have a strong relationship to one another. As an example, the
+/// [BottomNavigationBar] may use this animation to transition the currently
+/// displayed content when a new [BottomNavigationBarItem] is selected.
+///
+/// Scale is only applied to incoming elements to emphasize new content over
+/// old.
+///
+/// Consider using [FadeThroughPageTransitionsBuilder] within a
+/// [PageTransitionsTheme] if you want to apply this kind of transition to
+/// [MaterialPageRoute] transitions within a Navigator (see
+/// [FadeThroughPageTransitionsBuilder] for some example code). Or use this transition
+/// directly in a [PageTransitionSwitcher.transitionBuilder] to transition
+/// from one widget to another as seen in the following example:
+///
+/// ```dart
+/// int _selectedIndex = 0;
+///
+/// final List<Color> _colors = [Colors.blue, Colors.red, Colors.yellow];
+///
+/// @override
+/// Widget build(BuildContext context) {
+/// return Scaffold(
+/// appBar: AppBar(
+/// title: const Text('Switcher Sample'),
+/// ),
+/// body: PageTransitionSwitcher(
+/// transitionBuilder: (
+/// Widget child,
+/// Animation<double> primaryAnimation,
+/// Animation<double> secondaryAnimation,
+/// ) {
+/// return FadeThroughTransition(
+/// child: child,
+/// animation: primaryAnimation,
+/// secondaryAnimation: secondaryAnimation,
+/// );
+/// },
+/// child: Container(
+/// key: ValueKey<int>(_selectedIndex),
+/// color: _colors[_selectedIndex],
+/// ),
+/// ),
+/// bottomNavigationBar: BottomNavigationBar(
+/// items: const <BottomNavigationBarItem>[
+/// BottomNavigationBarItem(
+/// icon: Icon(Icons.home),
+/// title: Text('Blue'),
+/// ),
+/// BottomNavigationBarItem(
+/// icon: Icon(Icons.business),
+/// title: Text('Red'),
+/// ),
+/// BottomNavigationBarItem(
+/// icon: Icon(Icons.school),
+/// title: Text('Yellow'),
+/// ),
+/// ],
+/// currentIndex: _selectedIndex,
+/// selectedItemColor: Colors.amber[800],
+/// onTap: (int index) {
+/// setState(() {
+/// _selectedIndex = index;
+/// });
+/// },
+/// ),
+/// );
+/// }
+/// ```
+class FadeThroughTransition extends StatelessWidget {
+ /// Creates a [FadeThroughTransition].
+ ///
+ /// The [animation] and [secondaryAnimation] argument are required and must
+ /// not be null.
+ const FadeThroughTransition({
+ Key? key,
+ required this.animation,
+ required this.secondaryAnimation,
+ this.fillColor,
+ this.child,
+ }) : super(key: key);
+
+ /// The animation that drives the [child]'s entrance and exit.
+ ///
+ /// See also:
+ ///
+ /// * [TransitionRoute.animate], which is the value given to this property
+ /// when the [FadeThroughTransition] is used as a page transition.
+ final Animation<double> animation;
+
+ /// The animation that transitions [child] when new content is pushed on top
+ /// of it.
+ ///
+ /// See also:
+ ///
+ /// * [TransitionRoute.secondaryAnimation], which is the value given to this
+ // property when the [FadeThroughTransition] is used as a page transition.
+ final Animation<double> secondaryAnimation;
+
+ /// The color to use for the background color during the transition.
+ ///
+ /// This defaults to the [Theme]'s [ThemeData.canvasColor].
+ final Color? fillColor;
+
+ /// The widget below this widget in the tree.
+ ///
+ /// This widget will transition in and out as driven by [animation] and
+ /// [secondaryAnimation].
+ final Widget? child;
+
+ @override
+ Widget build(BuildContext context) {
+ return _ZoomedFadeInFadeOut(
+ animation: animation,
+ child: Container(
+ color: fillColor ?? Theme.of(context).canvasColor,
+ child: _ZoomedFadeInFadeOut(
+ animation: ReverseAnimation(secondaryAnimation),
+ child: child,
+ ),
+ ),
+ );
+ }
+}
+
+class _ZoomedFadeInFadeOut extends StatelessWidget {
+ const _ZoomedFadeInFadeOut({Key? key, required this.animation, this.child})
+ : super(key: key);
+
+ final Animation<double> animation;
+ final Widget? child;
+
+ @override
+ Widget build(BuildContext context) {
+ return DualTransitionBuilder(
+ animation: animation,
+ forwardBuilder: (
+ BuildContext context,
+ Animation<double> animation,
+ Widget? child,
+ ) {
+ return _ZoomedFadeIn(
+ animation: animation,
+ child: child,
+ );
+ },
+ reverseBuilder: (
+ BuildContext context,
+ Animation<double> animation,
+ Widget? child,
+ ) {
+ return _FadeOut(
+ child: child,
+ animation: animation,
+ );
+ },
+ child: child,
+ );
+ }
+}
+
+class _ZoomedFadeIn extends StatelessWidget {
+ const _ZoomedFadeIn({
+ this.child,
+ required this.animation,
+ });
+
+ final Widget? child;
+ final Animation<double> animation;
+
+ static final CurveTween _inCurve = CurveTween(
+ curve: const Cubic(0.0, 0.0, 0.2, 1.0),
+ );
+ static final TweenSequence<double> _scaleIn = TweenSequence<double>(
+ <TweenSequenceItem<double>>[
+ TweenSequenceItem<double>(
+ tween: ConstantTween<double>(0.92),
+ weight: 6 / 20,
+ ),
+ TweenSequenceItem<double>(
+ tween: Tween<double>(begin: 0.92, end: 1.0).chain(_inCurve),
+ weight: 14 / 20,
+ ),
+ ],
+ );
+ static final TweenSequence<double> _fadeInOpacity = TweenSequence<double>(
+ <TweenSequenceItem<double>>[
+ TweenSequenceItem<double>(
+ tween: ConstantTween<double>(0.0),
+ weight: 6 / 20,
+ ),
+ TweenSequenceItem<double>(
+ tween: Tween<double>(begin: 0.0, end: 1.0).chain(_inCurve),
+ weight: 14 / 20,
+ ),
+ ],
+ );
+
+ @override
+ Widget build(BuildContext context) {
+ return FadeTransition(
+ opacity: _fadeInOpacity.animate(animation),
+ child: ScaleTransition(
+ scale: _scaleIn.animate(animation),
+ child: child,
+ ),
+ );
+ }
+}
+
+class _FadeOut extends StatelessWidget {
+ const _FadeOut({
+ this.child,
+ required this.animation,
+ });
+
+ final Widget? child;
+ final Animation<double> animation;
+
+ static final CurveTween _outCurve = CurveTween(
+ curve: const Cubic(0.4, 0.0, 1.0, 1.0),
+ );
+ static final TweenSequence<double> _fadeOutOpacity = TweenSequence<double>(
+ <TweenSequenceItem<double>>[
+ TweenSequenceItem<double>(
+ tween: Tween<double>(begin: 1.0, end: 0.0).chain(_outCurve),
+ weight: 6 / 20,
+ ),
+ TweenSequenceItem<double>(
+ tween: ConstantTween<double>(0.0),
+ weight: 14 / 20,
+ ),
+ ],
+ );
+
+ @override
+ Widget build(BuildContext context) {
+ return FadeTransition(
+ opacity: _fadeOutOpacity.animate(animation),
+ child: child,
+ );
+ }
+}
diff --git a/packages/animations/lib/src/modal.dart b/packages/animations/lib/src/modal.dart
new file mode 100644
index 0000000..65b6f60
--- /dev/null
+++ b/packages/animations/lib/src/modal.dart
@@ -0,0 +1,224 @@
+// 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 'dart:ui' as ui;
+
+import 'package:flutter/material.dart';
+
+import 'fade_scale_transition.dart';
+
+/// Signature for a function that creates a widget that builds a
+/// transition.
+///
+/// Used by [PopupRoute].
+typedef _ModalTransitionBuilder = Widget Function(
+ BuildContext context,
+ Animation<double> animation,
+ Animation<double> secondaryAnimation,
+ Widget child,
+);
+
+/// Displays a modal above the current contents of the app.
+///
+/// Content below the modal is dimmed with a [ModalBarrier].
+///
+/// The `context` argument is used to look up the [Navigator] for the
+/// modal. It is only used when the method is called. Its corresponding widget
+/// can be safely removed from the tree before the modal is closed.
+///
+/// The `configuration` argument is used to determine characteristics of the
+/// modal route that will be displayed, such as the enter and exit
+/// transitions, the duration of the transitions, and modal barrier
+/// properties. By default, `configuration` is
+/// [FadeScaleTransitionConfiguration].
+///
+/// The `useRootNavigator` argument is used to determine whether to push the
+/// modal to the [Navigator] furthest from or nearest to the given `context`.
+/// By default, `useRootNavigator` is `true` and the modal route created by
+/// this method is pushed to the root navigator. If the application has
+/// multiple [Navigator] objects, it may be necessary to call
+/// `Navigator.of(context, rootNavigator: true).pop(result)` to close the
+/// modal rather than just `Navigator.pop(context, result)`.
+///
+/// Returns a [Future] that resolves to the value (if any) that was passed to
+/// [Navigator.pop] when the modal was closed.
+///
+/// See also:
+///
+/// * [ModalConfiguration], which is the configuration object used to define
+/// the modal's characteristics.
+Future<T?> showModal<T>({
+ required BuildContext context,
+ ModalConfiguration configuration = const FadeScaleTransitionConfiguration(),
+ bool useRootNavigator = true,
+ required WidgetBuilder builder,
+ RouteSettings? routeSettings,
+ ui.ImageFilter? filter,
+}) {
+ String? barrierLabel = configuration.barrierLabel;
+ // Avoid looking up [MaterialLocalizations.of(context).modalBarrierDismissLabel]
+ // if there is no dismissible barrier.
+ if (configuration.barrierDismissible && configuration.barrierLabel == null) {
+ barrierLabel = MaterialLocalizations.of(context).modalBarrierDismissLabel;
+ }
+ assert(!configuration.barrierDismissible || barrierLabel != null);
+ return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(
+ _ModalRoute<T>(
+ barrierColor: configuration.barrierColor,
+ barrierDismissible: configuration.barrierDismissible,
+ barrierLabel: barrierLabel,
+ transitionBuilder: configuration.transitionBuilder,
+ transitionDuration: configuration.transitionDuration,
+ reverseTransitionDuration: configuration.reverseTransitionDuration,
+ builder: builder,
+ routeSettings: routeSettings,
+ filter: filter,
+ ),
+ );
+}
+
+// A modal route that overlays a widget on the current route.
+class _ModalRoute<T> extends PopupRoute<T> {
+ /// Creates a route with general modal route.
+ ///
+ /// [barrierDismissible] configures whether or not tapping the modal's
+ /// scrim dismisses the modal. [barrierLabel] sets the semantic label for
+ /// a dismissible barrier. [barrierDismissible] cannot be null. If
+ /// [barrierDismissible] is true, the [barrierLabel] cannot be null.
+ ///
+ /// [transitionBuilder] takes in a function that creates a widget. This
+ /// widget is typically used to configure the modal's transition.
+ _ModalRoute({
+ this.barrierColor,
+ this.barrierDismissible = true,
+ this.barrierLabel,
+ required this.transitionDuration,
+ required this.reverseTransitionDuration,
+ required _ModalTransitionBuilder transitionBuilder,
+ required this.builder,
+ RouteSettings? routeSettings,
+ ui.ImageFilter? filter,
+ }) : assert(!barrierDismissible || barrierLabel != null),
+ _transitionBuilder = transitionBuilder,
+ super(filter: filter, settings: routeSettings);
+
+ @override
+ final Color? barrierColor;
+
+ @override
+ final bool barrierDismissible;
+
+ @override
+ final String? barrierLabel;
+
+ @override
+ final Duration transitionDuration;
+
+ @override
+ final Duration reverseTransitionDuration;
+
+ /// The primary contents of the modal.
+ final WidgetBuilder builder;
+
+ final _ModalTransitionBuilder _transitionBuilder;
+
+ @override
+ Widget buildPage(
+ BuildContext context,
+ Animation<double> animation,
+ Animation<double> secondaryAnimation,
+ ) {
+ final ThemeData theme = Theme.of(context);
+ return Semantics(
+ child: SafeArea(
+ child: Builder(
+ builder: (BuildContext context) {
+ final Widget child = Builder(builder: builder);
+ return Theme(data: theme, child: child);
+ },
+ ),
+ ),
+ scopesRoute: true,
+ explicitChildNodes: true,
+ );
+ }
+
+ @override
+ Widget buildTransitions(
+ BuildContext context,
+ Animation<double> animation,
+ Animation<double> secondaryAnimation,
+ Widget child,
+ ) {
+ return _transitionBuilder(
+ context,
+ animation,
+ secondaryAnimation,
+ child,
+ );
+ }
+}
+
+/// A configuration object containing the properties needed to implement a
+/// modal route.
+///
+/// The `barrierDismissible` argument is used to determine whether this route
+/// can be dismissed by tapping the modal barrier. This argument defaults
+/// to true. If `barrierDismissible` is true, a non-null `barrierLabel` must be
+/// provided.
+///
+/// The `barrierLabel` argument is the semantic label used for a dismissible
+/// barrier. This argument defaults to "Dismiss".
+abstract class ModalConfiguration {
+ /// Creates a modal configuration object that provides the necessary
+ /// properties to implement a modal route.
+ ///
+ /// [barrierDismissible] configures whether or not tapping the modal's
+ /// scrim dismisses the modal. [barrierLabel] sets the semantic label for
+ /// a dismissible barrier. [barrierDismissible] cannot be null. If
+ /// [barrierDismissible] is true, the [barrierLabel] cannot be null.
+ ///
+ /// [transitionDuration] and [reverseTransitionDuration] determine the
+ /// duration of the transitions when the modal enters and exits the
+ /// application. [transitionDuration] and [reverseTransitionDuration]
+ /// cannot be null.
+ const ModalConfiguration({
+ required this.barrierColor,
+ required this.barrierDismissible,
+ this.barrierLabel,
+ required this.transitionDuration,
+ required this.reverseTransitionDuration,
+ }) : assert(!barrierDismissible || barrierLabel != null);
+
+ /// The color to use for the modal barrier. If this is null, the barrier will
+ /// be transparent.
+ final Color barrierColor;
+
+ /// Whether you can dismiss this route by tapping the modal barrier.
+ final bool barrierDismissible;
+
+ /// The semantic label used for a dismissible barrier.
+ final String? barrierLabel;
+
+ /// The duration of the transition running forwards.
+ final Duration transitionDuration;
+
+ /// The duration of the transition running in reverse.
+ final Duration reverseTransitionDuration;
+
+ /// A builder that defines how the route arrives on and leaves the screen.
+ ///
+ /// The [buildTransitions] method is typically used to define transitions
+ /// that animate the new topmost route's comings and goings. When the
+ /// [Navigator] pushes a route on the top of its stack, the new route's
+ /// primary [animation] runs from 0.0 to 1.0. When the [Navigator] pops the
+ /// topmost route, e.g. because the use pressed the back button, the
+ /// primary animation runs from 1.0 to 0.0.
+ Widget transitionBuilder(
+ BuildContext context,
+ Animation<double> animation,
+ Animation<double> secondaryAnimation,
+ Widget child,
+ );
+}
diff --git a/packages/animations/lib/src/open_container.dart b/packages/animations/lib/src/open_container.dart
new file mode 100644
index 0000000..7b243ba
--- /dev/null
+++ b/packages/animations/lib/src/open_container.dart
@@ -0,0 +1,903 @@
+// 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/material.dart';
+import 'package:flutter/scheduler.dart';
+
+/// Signature for `action` callback function provided to [OpenContainer.openBuilder].
+///
+/// Parameter `returnValue` is the value which will be provided to [OpenContainer.onClosed]
+/// when `action` is called.
+typedef CloseContainerActionCallback<S> = void Function({S? returnValue});
+
+/// Signature for a function that creates a [Widget] in open state within an
+/// [OpenContainer].
+///
+/// The `action` callback provided to [OpenContainer.openBuilder] can be used
+/// to close the container.
+typedef OpenContainerBuilder<S> = Widget Function(
+ BuildContext context,
+ CloseContainerActionCallback<S> action,
+);
+
+/// Signature for a function that creates a [Widget] in closed state within an
+/// [OpenContainer].
+///
+/// The `action` callback provided to [OpenContainer.closedBuilder] can be used
+/// to open the container.
+typedef CloseContainerBuilder = Widget Function(
+ BuildContext context,
+ VoidCallback action,
+);
+
+/// The [OpenContainer] widget's fade transition type.
+///
+/// This determines the type of fade transition that the incoming and outgoing
+/// contents will use.
+enum ContainerTransitionType {
+ /// Fades the incoming element in over the outgoing element.
+ fade,
+
+ /// First fades the outgoing element out, and starts fading the incoming
+ /// element in once the outgoing element has completely faded out.
+ fadeThrough,
+}
+
+/// Callback function which is called when the [OpenContainer]
+/// is closed.
+typedef ClosedCallback<S> = void Function(S data);
+
+/// A container that grows to fill the screen to reveal new content when tapped.
+///
+/// While the container is closed, it shows the [Widget] returned by
+/// [closedBuilder]. When the container is tapped it grows to fill the entire
+/// size of the surrounding [Navigator] while fading out the widget returned by
+/// [closedBuilder] and fading in the widget returned by [openBuilder]. When the
+/// container is closed again via the callback provided to [openBuilder] or via
+/// Android's back button, the animation is reversed: The container shrinks back
+/// to its original size while the widget returned by [openBuilder] is faded out
+/// and the widget returned by [openBuilder] is faded back in.
+///
+/// By default, the container is in the closed state. During the transition from
+/// closed to open and vice versa the widgets returned by the [openBuilder] and
+/// [closedBuilder] exist in the tree at the same time. Therefore, the widgets
+/// returned by these builders cannot include the same global key.
+///
+/// `T` refers to the type of data returned by the route when the container
+/// is closed. This value can be accessed in the `onClosed` function.
+///
+// TODO(goderbauer): Add example animations and sample code.
+///
+/// See also:
+///
+/// * [Transitions with animated containers](https://material.io/design/motion/choreography.html#transformation)
+/// in the Material spec.
+@optionalTypeArgs
+class OpenContainer<T extends Object?> extends StatefulWidget {
+ /// Creates an [OpenContainer].
+ ///
+ /// All arguments except for [key] must not be null. The arguments
+ /// [openBuilder] and [closedBuilder] are required.
+ const OpenContainer({
+ Key? key,
+ this.closedColor = Colors.white,
+ this.openColor = Colors.white,
+ this.middleColor,
+ this.closedElevation = 1.0,
+ this.openElevation = 4.0,
+ this.closedShape = const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(4.0)),
+ ),
+ this.openShape = const RoundedRectangleBorder(),
+ this.onClosed,
+ required this.closedBuilder,
+ required this.openBuilder,
+ this.tappable = true,
+ this.transitionDuration = const Duration(milliseconds: 300),
+ this.transitionType = ContainerTransitionType.fade,
+ this.useRootNavigator = false,
+ this.routeSettings,
+ this.clipBehavior = Clip.antiAlias,
+ }) : super(key: key);
+
+ /// Background color of the container while it is closed.
+ ///
+ /// When the container is opened, it will first transition from this color
+ /// to [middleColor] and then transition from there to [openColor] in one
+ /// smooth animation. When the container is closed, it will transition back to
+ /// this color from [openColor] via [middleColor].
+ ///
+ /// Defaults to [Colors.white].
+ ///
+ /// See also:
+ ///
+ /// * [Material.color], which is used to implement this property.
+ final Color closedColor;
+
+ /// Background color of the container while it is open.
+ ///
+ /// When the container is closed, it will first transition from [closedColor]
+ /// to [middleColor] and then transition from there to this color in one
+ /// smooth animation. When the container is closed, it will transition back to
+ /// [closedColor] from this color via [middleColor].
+ ///
+ /// Defaults to [Colors.white].
+ ///
+ /// See also:
+ ///
+ /// * [Material.color], which is used to implement this property.
+ final Color openColor;
+
+ /// The color to use for the background color during the transition
+ /// with [ContainerTransitionType.fadeThrough].
+ ///
+ /// Defaults to [Theme]'s [ThemeData.canvasColor].
+ ///
+ /// See also:
+ ///
+ /// * [Material.color], which is used to implement this property.
+ final Color? middleColor;
+
+ /// Elevation of the container while it is closed.
+ ///
+ /// When the container is opened, it will transition from this elevation to
+ /// [openElevation]. When the container is closed, it will transition back
+ /// from [openElevation] to this elevation.
+ ///
+ /// Defaults to 1.0.
+ ///
+ /// See also:
+ ///
+ /// * [Material.elevation], which is used to implement this property.
+ final double closedElevation;
+
+ /// Elevation of the container while it is open.
+ ///
+ /// When the container is opened, it will transition to this elevation from
+ /// [closedElevation]. When the container is closed, it will transition back
+ /// from this elevation to [closedElevation].
+ ///
+ /// Defaults to 4.0.
+ ///
+ /// See also:
+ ///
+ /// * [Material.elevation], which is used to implement this property.
+ final double openElevation;
+
+ /// Shape of the container while it is closed.
+ ///
+ /// When the container is opened it will transition from this shape to
+ /// [openShape]. When the container is closed, it will transition back to this
+ /// shape.
+ ///
+ /// Defaults to a [RoundedRectangleBorder] with a [Radius.circular] of 4.0.
+ ///
+ /// See also:
+ ///
+ /// * [Material.shape], which is used to implement this property.
+ final ShapeBorder closedShape;
+
+ /// Shape of the container while it is open.
+ ///
+ /// When the container is opened it will transition from [closedShape] to
+ /// this shape. When the container is closed, it will transition from this
+ /// shape back to [closedShape].
+ ///
+ /// Defaults to a rectangular.
+ ///
+ /// See also:
+ ///
+ /// * [Material.shape], which is used to implement this property.
+ final ShapeBorder openShape;
+
+ /// Called when the container was popped and has returned to the closed state.
+ ///
+ /// The return value from the popped screen is passed to this function as an
+ /// argument.
+ ///
+ /// If no value is returned via [Navigator.pop] or [OpenContainer.openBuilder.action],
+ /// `null` will be returned by default.
+ final ClosedCallback<T?>? onClosed;
+
+ /// Called to obtain the child for the container in the closed state.
+ ///
+ /// The [Widget] returned by this builder is faded out when the container
+ /// opens and at the same time the widget returned by [openBuilder] is faded
+ /// in while the container grows to fill the surrounding [Navigator].
+ ///
+ /// The `action` callback provided to the builder can be called to open the
+ /// container.
+ final CloseContainerBuilder closedBuilder;
+
+ /// Called to obtain the child for the container in the open state.
+ ///
+ /// The [Widget] returned by this builder is faded in when the container
+ /// opens and at the same time the widget returned by [closedBuilder] is
+ /// faded out while the container grows to fill the surrounding [Navigator].
+ ///
+ /// The `action` callback provided to the builder can be called to close the
+ /// container.
+ final OpenContainerBuilder<T> openBuilder;
+
+ /// Whether the entire closed container can be tapped to open it.
+ ///
+ /// Defaults to true.
+ ///
+ /// When this is set to false the container can only be opened by calling the
+ /// `action` callback that is provided to the [closedBuilder].
+ final bool tappable;
+
+ /// The time it will take to animate the container from its closed to its
+ /// open state and vice versa.
+ ///
+ /// Defaults to 300ms.
+ final Duration transitionDuration;
+
+ /// The type of fade transition that the container will use for its
+ /// incoming and outgoing widgets.
+ ///
+ /// Defaults to [ContainerTransitionType.fade].
+ final ContainerTransitionType transitionType;
+
+ /// The [useRootNavigator] argument is used to determine whether to push the
+ /// route for [openBuilder] to the Navigator furthest from or nearest to
+ /// the given context.
+ ///
+ /// By default, [useRootNavigator] is false and the route created will push
+ /// to the nearest navigator.
+ final bool useRootNavigator;
+
+ /// Provides additional data to the [openBuilder] route pushed by the Navigator.
+ final RouteSettings? routeSettings;
+
+ /// The [closedBuilder] will be clipped (or not) according to this option.
+ ///
+ /// Defaults to [Clip.antiAlias], and must not be null.
+ ///
+ /// See also:
+ ///
+ /// * [Material.clipBehavior], which is used to implement this property.
+ final Clip clipBehavior;
+
+ @override
+ _OpenContainerState<T> createState() => _OpenContainerState<T>();
+}
+
+class _OpenContainerState<T> extends State<OpenContainer<T?>> {
+ // Key used in [_OpenContainerRoute] to hide the widget returned by
+ // [OpenContainer.openBuilder] in the source route while the container is
+ // opening/open. A copy of that widget is included in the
+ // [_OpenContainerRoute] where it fades out. To avoid issues with double
+ // shadows and transparency, we hide it in the source route.
+ final GlobalKey<_HideableState> _hideableKey = GlobalKey<_HideableState>();
+
+ // Key used to steal the state of the widget returned by
+ // [OpenContainer.openBuilder] from the source route and attach it to the
+ // same widget included in the [_OpenContainerRoute] where it fades out.
+ final GlobalKey _closedBuilderKey = GlobalKey();
+
+ Future<void> openContainer() async {
+ final Color middleColor =
+ widget.middleColor ?? Theme.of(context).canvasColor;
+ final T? data = await Navigator.of(
+ context,
+ rootNavigator: widget.useRootNavigator,
+ ).push(_OpenContainerRoute<T>(
+ closedColor: widget.closedColor,
+ openColor: widget.openColor,
+ middleColor: middleColor,
+ closedElevation: widget.closedElevation,
+ openElevation: widget.openElevation,
+ closedShape: widget.closedShape,
+ openShape: widget.openShape,
+ closedBuilder: widget.closedBuilder,
+ openBuilder: widget.openBuilder,
+ hideableKey: _hideableKey,
+ closedBuilderKey: _closedBuilderKey,
+ transitionDuration: widget.transitionDuration,
+ transitionType: widget.transitionType,
+ useRootNavigator: widget.useRootNavigator,
+ routeSettings: widget.routeSettings,
+ ));
+ if (widget.onClosed != null) {
+ widget.onClosed!(data);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return _Hideable(
+ key: _hideableKey,
+ child: GestureDetector(
+ onTap: widget.tappable ? openContainer : null,
+ child: Material(
+ clipBehavior: widget.clipBehavior,
+ color: widget.closedColor,
+ elevation: widget.closedElevation,
+ shape: widget.closedShape,
+ child: Builder(
+ key: _closedBuilderKey,
+ builder: (BuildContext context) {
+ return widget.closedBuilder(context, openContainer);
+ },
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+/// Controls the visibility of its child.
+///
+/// The child can be in one of three states:
+///
+/// * It is included in the tree and fully visible. (The `placeholderSize` is
+/// null and `isVisible` is true.)
+/// * It is included in the tree, but not visible; its size is maintained.
+/// (The `placeholderSize` is null and `isVisible` is false.)
+/// * It is not included in the tree. Instead a [SizedBox] of dimensions
+/// specified by `placeholderSize` is included in the tree. (The value of
+/// `isVisible` is ignored).
+class _Hideable extends StatefulWidget {
+ const _Hideable({
+ Key? key,
+ this.child,
+ }) : super(key: key);
+
+ final Widget? child;
+
+ @override
+ State<_Hideable> createState() => _HideableState();
+}
+
+class _HideableState extends State<_Hideable> {
+ /// When non-null the child is replaced by a [SizedBox] of the set size.
+ Size? get placeholderSize => _placeholderSize;
+ Size? _placeholderSize;
+ set placeholderSize(Size? value) {
+ if (_placeholderSize == value) {
+ return;
+ }
+ setState(() {
+ _placeholderSize = value;
+ });
+ }
+
+ /// When true the child is not visible, but will maintain its size.
+ ///
+ /// The value of this property is ignored when [placeholderSize] is non-null
+ /// (i.e. [isInTree] returns false).
+ bool get isVisible => _visible;
+ bool _visible = true;
+ set isVisible(bool value) {
+ if (_visible == value) {
+ return;
+ }
+ setState(() {
+ _visible = value;
+ });
+ }
+
+ /// Whether the child is currently included in the tree.
+ ///
+ /// When it is included, it may be visible or not according to [isVisible].
+ bool get isInTree => _placeholderSize == null;
+
+ @override
+ Widget build(BuildContext context) {
+ if (_placeholderSize != null) {
+ return SizedBox.fromSize(size: _placeholderSize);
+ }
+ return Opacity(
+ opacity: _visible ? 1.0 : 0.0,
+ child: widget.child,
+ );
+ }
+}
+
+class _OpenContainerRoute<T> extends ModalRoute<T> {
+ _OpenContainerRoute({
+ required this.closedColor,
+ required this.openColor,
+ required this.middleColor,
+ required double closedElevation,
+ required this.openElevation,
+ required ShapeBorder closedShape,
+ required this.openShape,
+ required this.closedBuilder,
+ required this.openBuilder,
+ required this.hideableKey,
+ required this.closedBuilderKey,
+ required this.transitionDuration,
+ required this.transitionType,
+ required this.useRootNavigator,
+ required RouteSettings? routeSettings,
+ }) : _elevationTween = Tween<double>(
+ begin: closedElevation,
+ end: openElevation,
+ ),
+ _shapeTween = ShapeBorderTween(
+ begin: closedShape,
+ end: openShape,
+ ),
+ _colorTween = _getColorTween(
+ transitionType: transitionType,
+ closedColor: closedColor,
+ openColor: openColor,
+ middleColor: middleColor,
+ ),
+ _closedOpacityTween = _getClosedOpacityTween(transitionType),
+ _openOpacityTween = _getOpenOpacityTween(transitionType),
+ super(settings: routeSettings);
+
+ static _FlippableTweenSequence<Color?> _getColorTween({
+ required ContainerTransitionType transitionType,
+ required Color closedColor,
+ required Color openColor,
+ required Color middleColor,
+ }) {
+ switch (transitionType) {
+ case ContainerTransitionType.fade:
+ return _FlippableTweenSequence<Color?>(
+ <TweenSequenceItem<Color?>>[
+ TweenSequenceItem<Color>(
+ tween: ConstantTween<Color>(closedColor),
+ weight: 1 / 5,
+ ),
+ TweenSequenceItem<Color?>(
+ tween: ColorTween(begin: closedColor, end: openColor),
+ weight: 1 / 5,
+ ),
+ TweenSequenceItem<Color>(
+ tween: ConstantTween<Color>(openColor),
+ weight: 3 / 5,
+ ),
+ ],
+ );
+ case ContainerTransitionType.fadeThrough:
+ return _FlippableTweenSequence<Color?>(
+ <TweenSequenceItem<Color?>>[
+ TweenSequenceItem<Color?>(
+ tween: ColorTween(begin: closedColor, end: middleColor),
+ weight: 1 / 5,
+ ),
+ TweenSequenceItem<Color?>(
+ tween: ColorTween(begin: middleColor, end: openColor),
+ weight: 4 / 5,
+ ),
+ ],
+ );
+ }
+ }
+
+ static _FlippableTweenSequence<double> _getClosedOpacityTween(
+ ContainerTransitionType transitionType) {
+ switch (transitionType) {
+ case ContainerTransitionType.fade:
+ return _FlippableTweenSequence<double>(
+ <TweenSequenceItem<double>>[
+ TweenSequenceItem<double>(
+ tween: ConstantTween<double>(1.0),
+ weight: 1,
+ ),
+ ],
+ );
+ case ContainerTransitionType.fadeThrough:
+ return _FlippableTweenSequence<double>(
+ <TweenSequenceItem<double>>[
+ TweenSequenceItem<double>(
+ tween: Tween<double>(begin: 1.0, end: 0.0),
+ weight: 1 / 5,
+ ),
+ TweenSequenceItem<double>(
+ tween: ConstantTween<double>(0.0),
+ weight: 4 / 5,
+ ),
+ ],
+ );
+ }
+ }
+
+ static _FlippableTweenSequence<double> _getOpenOpacityTween(
+ ContainerTransitionType transitionType) {
+ switch (transitionType) {
+ case ContainerTransitionType.fade:
+ return _FlippableTweenSequence<double>(
+ <TweenSequenceItem<double>>[
+ TweenSequenceItem<double>(
+ tween: ConstantTween<double>(0.0),
+ weight: 1 / 5,
+ ),
+ TweenSequenceItem<double>(
+ tween: Tween<double>(begin: 0.0, end: 1.0),
+ weight: 1 / 5,
+ ),
+ TweenSequenceItem<double>(
+ tween: ConstantTween<double>(1.0),
+ weight: 3 / 5,
+ ),
+ ],
+ );
+ case ContainerTransitionType.fadeThrough:
+ return _FlippableTweenSequence<double>(
+ <TweenSequenceItem<double>>[
+ TweenSequenceItem<double>(
+ tween: ConstantTween<double>(0.0),
+ weight: 1 / 5,
+ ),
+ TweenSequenceItem<double>(
+ tween: Tween<double>(begin: 0.0, end: 1.0),
+ weight: 4 / 5,
+ ),
+ ],
+ );
+ }
+ }
+
+ final Color closedColor;
+ final Color openColor;
+ final Color middleColor;
+ final double openElevation;
+ final ShapeBorder openShape;
+ final CloseContainerBuilder closedBuilder;
+ final OpenContainerBuilder<T> openBuilder;
+
+ // See [_OpenContainerState._hideableKey].
+ final GlobalKey<_HideableState> hideableKey;
+
+ // See [_OpenContainerState._closedBuilderKey].
+ final GlobalKey closedBuilderKey;
+
+ @override
+ final Duration transitionDuration;
+ final ContainerTransitionType transitionType;
+
+ final bool useRootNavigator;
+
+ final Tween<double> _elevationTween;
+ final ShapeBorderTween _shapeTween;
+ final _FlippableTweenSequence<double> _closedOpacityTween;
+ final _FlippableTweenSequence<double> _openOpacityTween;
+ final _FlippableTweenSequence<Color?> _colorTween;
+
+ static final TweenSequence<Color?> _scrimFadeInTween = TweenSequence<Color?>(
+ <TweenSequenceItem<Color?>>[
+ TweenSequenceItem<Color?>(
+ tween: ColorTween(begin: Colors.transparent, end: Colors.black54),
+ weight: 1 / 5,
+ ),
+ TweenSequenceItem<Color>(
+ tween: ConstantTween<Color>(Colors.black54),
+ weight: 4 / 5,
+ ),
+ ],
+ );
+ static final Tween<Color?> _scrimFadeOutTween = ColorTween(
+ begin: Colors.transparent,
+ end: Colors.black54,
+ );
+
+ // Key used for the widget returned by [OpenContainer.openBuilder] to keep
+ // its state when the shape of the widget tree is changed at the end of the
+ // animation to remove all the craft that was necessary to make the animation
+ // work.
+ final GlobalKey _openBuilderKey = GlobalKey();
+
+ // Defines the position and the size of the (opening) [OpenContainer] within
+ // the bounds of the enclosing [Navigator].
+ final RectTween _rectTween = RectTween();
+
+ AnimationStatus? _lastAnimationStatus;
+ AnimationStatus? _currentAnimationStatus;
+
+ @override
+ TickerFuture didPush() {
+ _takeMeasurements(navigatorContext: hideableKey.currentContext!);
+
+ animation!.addStatusListener((AnimationStatus status) {
+ _lastAnimationStatus = _currentAnimationStatus;
+ _currentAnimationStatus = status;
+ switch (status) {
+ case AnimationStatus.dismissed:
+ _toggleHideable(hide: false);
+ break;
+ case AnimationStatus.completed:
+ _toggleHideable(hide: true);
+ break;
+ case AnimationStatus.forward:
+ case AnimationStatus.reverse:
+ break;
+ }
+ });
+
+ return super.didPush();
+ }
+
+ @override
+ bool didPop(T? result) {
+ _takeMeasurements(
+ navigatorContext: subtreeContext!,
+ delayForSourceRoute: true,
+ );
+ return super.didPop(result);
+ }
+
+ @override
+ void dispose() {
+ if (hideableKey.currentState?.isVisible == false) {
+ // This route may be disposed without dismissing its animation if it is
+ // removed by the navigator.
+ SchedulerBinding.instance!
+ .addPostFrameCallback((Duration d) => _toggleHideable(hide: false));
+ }
+ super.dispose();
+ }
+
+ void _toggleHideable({required bool hide}) {
+ if (hideableKey.currentState != null) {
+ hideableKey.currentState!
+ ..placeholderSize = null
+ ..isVisible = !hide;
+ }
+ }
+
+ void _takeMeasurements({
+ required BuildContext navigatorContext,
+ bool delayForSourceRoute = false,
+ }) {
+ final RenderBox navigator = Navigator.of(
+ navigatorContext,
+ rootNavigator: useRootNavigator,
+ ).context.findRenderObject()! as RenderBox;
+ final Size navSize = _getSize(navigator);
+ _rectTween.end = Offset.zero & navSize;
+
+ void takeMeasurementsInSourceRoute([Duration? _]) {
+ if (!navigator.attached || hideableKey.currentContext == null) {
+ return;
+ }
+ _rectTween.begin = _getRect(hideableKey, navigator);
+ hideableKey.currentState!.placeholderSize = _rectTween.begin!.size;
+ }
+
+ if (delayForSourceRoute) {
+ SchedulerBinding.instance!
+ .addPostFrameCallback(takeMeasurementsInSourceRoute);
+ } else {
+ takeMeasurementsInSourceRoute();
+ }
+ }
+
+ Size _getSize(RenderBox render) {
+ assert(render.hasSize);
+ return render.size;
+ }
+
+ // Returns the bounds of the [RenderObject] identified by `key` in the
+ // coordinate system of `ancestor`.
+ Rect _getRect(GlobalKey key, RenderBox ancestor) {
+ assert(key.currentContext != null);
+ assert(ancestor.hasSize);
+ final RenderBox render =
+ key.currentContext!.findRenderObject()! as RenderBox;
+ assert(render.hasSize);
+ return MatrixUtils.transformRect(
+ render.getTransformTo(ancestor),
+ Offset.zero & render.size,
+ );
+ }
+
+ bool get _transitionWasInterrupted {
+ bool wasInProgress = false;
+ bool isInProgress = false;
+
+ switch (_currentAnimationStatus) {
+ case AnimationStatus.completed:
+ case AnimationStatus.dismissed:
+ isInProgress = false;
+ break;
+ case AnimationStatus.forward:
+ case AnimationStatus.reverse:
+ isInProgress = true;
+ break;
+ case null:
+ break;
+ }
+ switch (_lastAnimationStatus) {
+ case AnimationStatus.completed:
+ case AnimationStatus.dismissed:
+ wasInProgress = false;
+ break;
+ case AnimationStatus.forward:
+ case AnimationStatus.reverse:
+ wasInProgress = true;
+ break;
+ case null:
+ break;
+ }
+ return wasInProgress && isInProgress;
+ }
+
+ void closeContainer({T? returnValue}) {
+ Navigator.of(subtreeContext!).pop(returnValue);
+ }
+
+ @override
+ Widget buildPage(
+ BuildContext context,
+ Animation<double> animation,
+ Animation<double> secondaryAnimation,
+ ) {
+ return Align(
+ alignment: Alignment.topLeft,
+ child: AnimatedBuilder(
+ animation: animation,
+ builder: (BuildContext context, Widget? child) {
+ if (animation.isCompleted) {
+ return SizedBox.expand(
+ child: Material(
+ color: openColor,
+ elevation: openElevation,
+ shape: openShape,
+ child: Builder(
+ key: _openBuilderKey,
+ builder: (BuildContext context) {
+ return openBuilder(context, closeContainer);
+ },
+ ),
+ ),
+ );
+ }
+
+ final Animation<double> curvedAnimation = CurvedAnimation(
+ parent: animation,
+ curve: Curves.fastOutSlowIn,
+ reverseCurve:
+ _transitionWasInterrupted ? null : Curves.fastOutSlowIn.flipped,
+ );
+ TweenSequence<Color?>? colorTween;
+ TweenSequence<double>? closedOpacityTween, openOpacityTween;
+ Animatable<Color?>? scrimTween;
+ switch (animation.status) {
+ case AnimationStatus.dismissed:
+ case AnimationStatus.forward:
+ closedOpacityTween = _closedOpacityTween;
+ openOpacityTween = _openOpacityTween;
+ colorTween = _colorTween;
+ scrimTween = _scrimFadeInTween;
+ break;
+ case AnimationStatus.reverse:
+ if (_transitionWasInterrupted) {
+ closedOpacityTween = _closedOpacityTween;
+ openOpacityTween = _openOpacityTween;
+ colorTween = _colorTween;
+ scrimTween = _scrimFadeInTween;
+ break;
+ }
+ closedOpacityTween = _closedOpacityTween.flipped;
+ openOpacityTween = _openOpacityTween.flipped;
+ colorTween = _colorTween.flipped;
+ scrimTween = _scrimFadeOutTween;
+ break;
+ case AnimationStatus.completed:
+ assert(false); // Unreachable.
+ break;
+ }
+ assert(colorTween != null);
+ assert(closedOpacityTween != null);
+ assert(openOpacityTween != null);
+ assert(scrimTween != null);
+
+ final Rect rect = _rectTween.evaluate(curvedAnimation)!;
+ return SizedBox.expand(
+ child: Container(
+ color: scrimTween!.evaluate(curvedAnimation),
+ child: Align(
+ alignment: Alignment.topLeft,
+ child: Transform.translate(
+ offset: Offset(rect.left, rect.top),
+ child: SizedBox(
+ width: rect.width,
+ height: rect.height,
+ child: Material(
+ clipBehavior: Clip.antiAlias,
+ animationDuration: Duration.zero,
+ color: colorTween!.evaluate(animation),
+ shape: _shapeTween.evaluate(curvedAnimation),
+ elevation: _elevationTween.evaluate(curvedAnimation),
+ child: Stack(
+ fit: StackFit.passthrough,
+ children: <Widget>[
+ // Closed child fading out.
+ FittedBox(
+ fit: BoxFit.fitWidth,
+ alignment: Alignment.topLeft,
+ child: SizedBox(
+ width: _rectTween.begin!.width,
+ height: _rectTween.begin!.height,
+ child: (hideableKey.currentState?.isInTree ??
+ false)
+ ? null
+ : Opacity(
+ opacity: closedOpacityTween!
+ .evaluate(animation),
+ child: Builder(
+ key: closedBuilderKey,
+ builder: (BuildContext context) {
+ // Use dummy "open container" callback
+ // since we are in the process of opening.
+ return closedBuilder(context, () {});
+ },
+ ),
+ ),
+ ),
+ ),
+
+ // Open child fading in.
+ FittedBox(
+ fit: BoxFit.fitWidth,
+ alignment: Alignment.topLeft,
+ child: SizedBox(
+ width: _rectTween.end!.width,
+ height: _rectTween.end!.height,
+ child: Opacity(
+ opacity: openOpacityTween!.evaluate(animation),
+ child: Builder(
+ key: _openBuilderKey,
+ builder: (BuildContext context) {
+ return openBuilder(context, closeContainer);
+ },
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ );
+ }
+
+ @override
+ bool get maintainState => true;
+
+ @override
+ Color? get barrierColor => null;
+
+ @override
+ bool get opaque => true;
+
+ @override
+ bool get barrierDismissible => false;
+
+ @override
+ String? get barrierLabel => null;
+}
+
+class _FlippableTweenSequence<T> extends TweenSequence<T> {
+ _FlippableTweenSequence(this._items) : super(_items);
+
+ final List<TweenSequenceItem<T>> _items;
+ _FlippableTweenSequence<T>? _flipped;
+
+ _FlippableTweenSequence<T>? get flipped {
+ if (_flipped == null) {
+ final List<TweenSequenceItem<T>> newItems = <TweenSequenceItem<T>>[];
+ for (int i = 0; i < _items.length; i++) {
+ newItems.add(TweenSequenceItem<T>(
+ tween: _items[i].tween,
+ weight: _items[_items.length - 1 - i].weight,
+ ));
+ }
+ _flipped = _FlippableTweenSequence<T>(newItems);
+ }
+ return _flipped;
+ }
+}
diff --git a/packages/animations/lib/src/page_transition_switcher.dart b/packages/animations/lib/src/page_transition_switcher.dart
new file mode 100644
index 0000000..ad00703
--- /dev/null
+++ b/packages/animations/lib/src/page_transition_switcher.dart
@@ -0,0 +1,438 @@
+// 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/animation.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+
+/// An internal representation of a child widget subtree that, now or in the past,
+/// was set on the [PageTransitionSwitcher.child] field and is now in the process of
+/// transitioning.
+///
+/// The internal representation includes fields that we don't want to expose to
+/// the public API (like the controllers).
+class _ChildEntry {
+ /// Creates a [_ChildEntry].
+ ///
+ /// The [primaryController], [secondaryController], [transition] and
+ /// [widgetChild] parameters must not be null.
+ _ChildEntry({
+ required this.primaryController,
+ required this.secondaryController,
+ required this.transition,
+ required this.widgetChild,
+ });
+
+ /// The animation controller for the child's transition.
+ final AnimationController primaryController;
+
+ /// The (curved) animation being used to drive the transition.
+ final AnimationController secondaryController;
+
+ /// The currently built transition for this child.
+ Widget transition;
+
+ /// The widget's child at the time this entry was created or updated.
+ /// Used to rebuild the transition if necessary.
+ Widget widgetChild;
+
+ /// Release the resources used by this object.
+ ///
+ /// The object is no longer usable after this method is called.
+ void dispose() {
+ primaryController.dispose();
+ secondaryController.dispose();
+ }
+
+ @override
+ String toString() {
+ return 'PageTransitionSwitcherEntry#${shortHash(this)}($widgetChild)';
+ }
+}
+
+/// Signature for builders used to generate custom layouts for
+/// [PageTransitionSwitcher].
+///
+/// The builder should return a widget which contains the given children, laid
+/// out as desired. It must not return null. The builder should be able to
+/// handle an empty list of `entries`.
+typedef PageTransitionSwitcherLayoutBuilder = Widget Function(
+ List<Widget> entries,
+);
+
+/// Signature for builders used to generate custom transitions for
+/// [PageTransitionSwitcher].
+///
+/// The function should return a widget which wraps the given `child`.
+///
+/// When a [PageTransitionSwitcher]'s `child` is replaced, the new child's
+/// `primaryAnimation` runs forward and the value of its `secondaryAnimation` is
+/// usually fixed at 0.0. At the same time, the old child's `secondaryAnimation`
+/// runs forward, and the value of its primaryAnimation is usually fixed at 1.0.
+///
+/// The widget returned by the [PageTransitionSwitcherTransitionBuilder] can
+/// incorporate both animations. It will use the primary animation to define how
+/// its child appears, and the secondary animation to define how its child
+/// disappears.
+typedef PageTransitionSwitcherTransitionBuilder = Widget Function(
+ Widget child,
+ Animation<double> primaryAnimation,
+ Animation<double> secondaryAnimation,
+);
+
+/// A widget that transitions from an old child to a new child whenever [child]
+/// changes using an animation specified by [transitionBuilder].
+///
+/// This is a variation of an [AnimatedSwitcher], but instead of using the
+/// same transition for enter and exit, two separate transitions can be
+/// specified, similar to how the enter and exit transitions of a [PageRoute]
+/// are defined.
+///
+/// When a new [child] is specified, the [transitionBuilder] is effectively
+/// applied twice, once to the old child and once to the new one. When
+/// [reverse] is false, the old child's `secondaryAnimation` runs forward, and
+/// the value of its `primaryAnimation` is usually fixed at 1.0. The new child's
+/// `primaryAnimation` runs forward and the value of its `secondaryAnimation` is
+/// usually fixed at 0.0. The widget returned by the [transitionBuilder] can
+/// incorporate both animations. It will use the primary animation to define how
+/// its child appears, and the secondary animation to define how its child
+/// disappears. This is similar to the transition associated with pushing a new
+/// [PageRoute] on top of another.
+///
+/// When [reverse] is true, the old child's `primaryAnimation` runs in reverse
+/// and the value of its `secondaryAnimation` is usually fixed at 0.0. The new
+/// child's `secondaryAnimation` runs in reverse and the value of its
+/// `primaryAnimation` is usually fixed at 1.0. This is similar to popping a
+/// [PageRoute] to reveal another [PageRoute] underneath it.
+///
+/// This process is the same as the one used by [PageRoute.buildTransitions].
+///
+/// The following example shows a [transitionBuilder] that slides out the
+/// old child to the right (driven by the `secondaryAnimation`) while the new
+/// child fades in (driven by the `primaryAnimation`):
+///
+/// ```dart
+/// transitionBuilder: (
+/// Widget child,
+/// Animation<double> primaryAnimation,
+/// Animation<double> secondaryAnimation,
+/// ) {
+/// return SlideTransition(
+/// position: Tween<Offset>(
+/// begin: Offset.zero,
+/// end: const Offset(1.5, 0.0),
+/// ).animate(secondaryAnimation),
+/// child: FadeTransition(
+/// opacity: Tween<double>(
+/// begin: 0.0,
+/// end: 1.0,
+/// ).animate(primaryAnimation),
+/// child: child,
+/// ),
+/// );
+/// },
+/// ```
+///
+/// If the children are swapped fast enough (i.e. before [duration] elapses),
+/// more than one old child can exist and be transitioning out while the
+/// newest one is transitioning in.
+///
+/// If the *new* child is the same widget type and key as the *old* child,
+/// but with different parameters, then [PageTransitionSwitcher] will *not* do a
+/// transition between them, since as far as the framework is concerned, they
+/// are the same widget and the existing widget can be updated with the new
+/// parameters. To force the transition to occur, set a [Key] on each child
+/// widget that you wish to be considered unique (typically a [ValueKey] on the
+/// widget data that distinguishes this child from the others). For example,
+/// changing the child from `SizedBox(width: 10)` to `SizedBox(width: 100)`
+/// would not trigger a transition but changing the child from
+/// `SizedBox(width: 10)` to `SizedBox(key: Key('foo'), width: 100)` would.
+/// Similarly, changing the child to `Container(width: 10)` would trigger a
+/// transition.
+///
+/// The same key can be used for a new child as was used for an already-outgoing
+/// child; the two will not be considered related. For example, if a progress
+/// indicator with key A is first shown, then an image with key B, then another
+/// progress indicator with key A again, all in rapid succession, then the old
+/// progress indicator and the image will be fading out while a new progress
+/// indicator is fading in.
+///
+/// PageTransitionSwitcher uses the [layoutBuilder] property to lay out the
+/// old and new child widgets. By default, [defaultLayoutBuilder] is used.
+/// See the documentation for [layoutBuilder] for suggestions on how to
+/// configure the layout of the incoming and outgoing child widgets if
+/// [defaultLayoutBuilder] is not your desired layout.
+class PageTransitionSwitcher extends StatefulWidget {
+ /// Creates a [PageTransitionSwitcher].
+ ///
+ /// The [duration], [reverse], and [transitionBuilder] parameters
+ /// must not be null.
+ const PageTransitionSwitcher({
+ Key? key,
+ this.duration = const Duration(milliseconds: 300),
+ this.reverse = false,
+ required this.transitionBuilder,
+ this.layoutBuilder = defaultLayoutBuilder,
+ this.child,
+ }) : super(key: key);
+
+ /// The current child widget to display.
+ ///
+ /// If there was an old child, it will be transitioned out using the
+ /// secondary animation of the [transitionBuilder], while the new child
+ /// transitions in using the primary animation of the [transitionBuilder].
+ ///
+ /// If there was no old child, then this child will transition in using
+ /// the primary animation of the [transitionBuilder].
+ ///
+ /// The child is considered to be "new" if it has a different type or [Key]
+ /// (see [Widget.canUpdate]).
+ final Widget? child;
+
+ /// The duration of the transition from the old [child] value to the new one.
+ ///
+ /// This duration is applied to the given [child] when that property is set to
+ /// a new child. Changing [duration] will not affect the durations of
+ /// transitions already in progress.
+ final Duration duration;
+
+ /// Indicates whether the new [child] will visually appear on top of or
+ /// underneath the old child.
+ ///
+ /// When this is false, the new child will transition in on top of the
+ /// old child while its primary animation and the secondary
+ /// animation of the old child are running forward. This is similar to
+ /// the transition associated with pushing a new [PageRoute] on top of
+ /// another.
+ ///
+ /// When this is true, the new child will transition in below the
+ /// old child while its secondary animation and the primary
+ /// animation of the old child are running in reverse. This is similar to
+ /// the transition associated with popping a [PageRoute] to reveal a new
+ /// [PageRoute] below it.
+ final bool reverse;
+
+ /// A function that wraps a new [child] with a primary and secondary animation
+ /// set define how the child appears and disappears.
+ ///
+ /// This is only called when a new [child] is set (not for each build), or
+ /// when a new [transitionBuilder] is set. If a new [transitionBuilder] is
+ /// set, then the transition is rebuilt for the current child and all old
+ /// children using the new [transitionBuilder]. The function must not return
+ /// null.
+ ///
+ /// The child provided to the transitionBuilder may be null.
+ final PageTransitionSwitcherTransitionBuilder transitionBuilder;
+
+ /// A function that wraps all of the children that are transitioning out, and
+ /// the [child] that's transitioning in, with a widget that lays all of them
+ /// out. This is called every time this widget is built. The function must not
+ /// return null.
+ ///
+ /// The default [PageTransitionSwitcherLayoutBuilder] used is
+ /// [defaultLayoutBuilder].
+ ///
+ /// The following example shows a [layoutBuilder] that places all entries in a
+ /// [Stack] that sizes itself to match the largest of the active entries.
+ /// All children are aligned on the top left corner of the [Stack].
+ ///
+ /// ```dart
+ /// PageTransitionSwitcher(
+ /// duration: const Duration(milliseconds: 100),
+ /// child: Container(color: Colors.red),
+ /// layoutBuilder: (
+ /// List<Widget> entries,
+ /// ) {
+ /// return Stack(
+ /// children: entries,
+ /// alignment: Alignment.topLeft,
+ /// );
+ /// },
+ /// ),
+ /// ```
+ /// See [PageTransitionSwitcherLayoutBuilder] for more information about
+ /// how a layout builder should function.
+ final PageTransitionSwitcherLayoutBuilder layoutBuilder;
+
+ /// The default layout builder for [PageTransitionSwitcher].
+ ///
+ /// This function is the default way for how the new and old child widgets are placed
+ /// during the transition between the two widgets. All children are placed in a
+ /// [Stack] that sizes itself to match the largest of the child or a previous child.
+ /// The children are centered on each other.
+ ///
+ /// See [PageTransitionSwitcherTransitionBuilder] for more information on the function
+ /// signature.
+ static Widget defaultLayoutBuilder(List<Widget> entries) {
+ return Stack(
+ children: entries,
+ alignment: Alignment.center,
+ );
+ }
+
+ @override
+ _PageTransitionSwitcherState createState() => _PageTransitionSwitcherState();
+}
+
+class _PageTransitionSwitcherState extends State<PageTransitionSwitcher>
+ with TickerProviderStateMixin {
+ final List<_ChildEntry> _activeEntries = <_ChildEntry>[];
+ _ChildEntry? _currentEntry;
+ int _childNumber = 0;
+
+ @override
+ void initState() {
+ super.initState();
+ _addEntryForNewChild(shouldAnimate: false);
+ }
+
+ @override
+ void didUpdateWidget(PageTransitionSwitcher oldWidget) {
+ super.didUpdateWidget(oldWidget);
+
+ // If the transition builder changed, then update all of the old
+ // transitions.
+ if (widget.transitionBuilder != oldWidget.transitionBuilder) {
+ _activeEntries.forEach(_updateTransitionForEntry);
+ }
+
+ final bool hasNewChild = widget.child != null;
+ final bool hasOldChild = _currentEntry != null;
+ if (hasNewChild != hasOldChild ||
+ hasNewChild &&
+ !Widget.canUpdate(widget.child!, _currentEntry!.widgetChild)) {
+ // Child has changed, fade current entry out and add new entry.
+ _childNumber += 1;
+ _addEntryForNewChild(shouldAnimate: true);
+ } else if (_currentEntry != null) {
+ assert(hasOldChild && hasNewChild);
+ assert(Widget.canUpdate(widget.child!, _currentEntry!.widgetChild));
+ // Child has been updated. Make sure we update the child widget and
+ // transition in _currentEntry even though we're not going to start a new
+ // animation, but keep the key from the old transition so that we
+ // update the transition instead of replacing it.
+ _currentEntry!.widgetChild = widget.child!;
+ _updateTransitionForEntry(_currentEntry!); // uses entry.widgetChild
+ }
+ }
+
+ void _addEntryForNewChild({required bool shouldAnimate}) {
+ assert(shouldAnimate || _currentEntry == null);
+ if (_currentEntry != null) {
+ assert(shouldAnimate);
+ if (widget.reverse) {
+ _currentEntry!.primaryController.reverse();
+ } else {
+ _currentEntry!.secondaryController.forward();
+ }
+ _currentEntry = null;
+ }
+ if (widget.child == null) {
+ return;
+ }
+ final AnimationController primaryController = AnimationController(
+ duration: widget.duration,
+ vsync: this,
+ );
+ final AnimationController secondaryController = AnimationController(
+ duration: widget.duration,
+ vsync: this,
+ );
+ if (shouldAnimate) {
+ if (widget.reverse) {
+ primaryController.value = 1.0;
+ secondaryController.value = 1.0;
+ secondaryController.reverse();
+ } else {
+ primaryController.forward();
+ }
+ } else {
+ assert(_activeEntries.isEmpty);
+ primaryController.value = 1.0;
+ }
+ _currentEntry = _newEntry(
+ child: widget.child!,
+ primaryController: primaryController,
+ secondaryController: secondaryController,
+ builder: widget.transitionBuilder,
+ );
+ if (widget.reverse && _activeEntries.isNotEmpty) {
+ // Add below old child.
+ _activeEntries.insert(_activeEntries.length - 1, _currentEntry!);
+ } else {
+ // Add on top of old child.
+ _activeEntries.add(_currentEntry!);
+ }
+ }
+
+ _ChildEntry _newEntry({
+ required Widget child,
+ required PageTransitionSwitcherTransitionBuilder builder,
+ required AnimationController primaryController,
+ required AnimationController secondaryController,
+ }) {
+ final Widget transition = builder(
+ child,
+ primaryController,
+ secondaryController,
+ );
+ final _ChildEntry entry = _ChildEntry(
+ widgetChild: child,
+ transition: KeyedSubtree.wrap(
+ transition,
+ _childNumber,
+ ),
+ primaryController: primaryController,
+ secondaryController: secondaryController,
+ );
+ secondaryController.addStatusListener((AnimationStatus status) {
+ if (status == AnimationStatus.completed) {
+ assert(mounted);
+ assert(_activeEntries.contains(entry));
+ setState(() {
+ _activeEntries.remove(entry);
+ entry.dispose();
+ });
+ }
+ });
+ primaryController.addStatusListener((AnimationStatus status) {
+ if (status == AnimationStatus.dismissed) {
+ assert(mounted);
+ assert(_activeEntries.contains(entry));
+ setState(() {
+ _activeEntries.remove(entry);
+ entry.dispose();
+ });
+ }
+ });
+ return entry;
+ }
+
+ void _updateTransitionForEntry(_ChildEntry entry) {
+ final Widget transition = widget.transitionBuilder(
+ entry.widgetChild,
+ entry.primaryController,
+ entry.secondaryController,
+ );
+ entry.transition = KeyedSubtree(
+ key: entry.transition.key,
+ child: transition,
+ );
+ }
+
+ @override
+ void dispose() {
+ for (final _ChildEntry entry in _activeEntries) {
+ entry.dispose();
+ }
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return widget.layoutBuilder(_activeEntries
+ .map<Widget>((_ChildEntry entry) => entry.transition)
+ .toList());
+ }
+}
diff --git a/packages/animations/lib/src/shared_axis_transition.dart b/packages/animations/lib/src/shared_axis_transition.dart
new file mode 100644
index 0000000..a8cf497
--- /dev/null
+++ b/packages/animations/lib/src/shared_axis_transition.dart
@@ -0,0 +1,483 @@
+// 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/animation.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+
+/// Determines which type of shared axis transition is used.
+enum SharedAxisTransitionType {
+ /// Creates a shared axis vertical (y-axis) page transition.
+ vertical,
+
+ /// Creates a shared axis horizontal (x-axis) page transition.
+ horizontal,
+
+ /// Creates a shared axis scaled (z-axis) page transition.
+ scaled,
+}
+
+/// Used by [PageTransitionsTheme] to define a page route transition animation
+/// in which outgoing and incoming elements share a fade transition.
+///
+/// The shared axis pattern provides the transition animation between UI elements
+/// that have a spatial or navigational relationship. For example,
+/// transitioning from one page of a sign up page to the next one.
+///
+/// The following example shows how the SharedAxisPageTransitionsBuilder can
+/// be used in a [PageTransitionsTheme] to change the default transitions
+/// of [MaterialPageRoute]s.
+///
+/// ```dart
+/// MaterialApp(
+/// theme: ThemeData(
+/// pageTransitionsTheme: PageTransitionsTheme(
+/// builders: {
+/// TargetPlatform.android: SharedAxisPageTransitionsBuilder(
+/// transitionType: SharedAxisTransitionType.horizontal,
+/// ),
+/// TargetPlatform.iOS: SharedAxisPageTransitionsBuilder(
+/// transitionType: SharedAxisTransitionType.horizontal,
+/// ),
+/// },
+/// ),
+/// ),
+/// routes: {
+/// '/': (BuildContext context) {
+/// return Container(
+/// color: Colors.red,
+/// child: Center(
+/// child: ElevatedButton(
+/// child: Text('Push route'),
+/// onPressed: () {
+/// Navigator.of(context).pushNamed('/a');
+/// },
+/// ),
+/// ),
+/// );
+/// },
+/// '/a' : (BuildContext context) {
+/// return Container(
+/// color: Colors.blue,
+/// child: Center(
+/// child: ElevatedButton(
+/// child: Text('Pop route'),
+/// onPressed: () {
+/// Navigator.of(context).pop();
+/// },
+/// ),
+/// ),
+/// );
+/// },
+/// },
+/// );
+/// ```
+class SharedAxisPageTransitionsBuilder extends PageTransitionsBuilder {
+ /// Construct a [SharedAxisPageTransitionsBuilder].
+ const SharedAxisPageTransitionsBuilder({
+ required this.transitionType,
+ this.fillColor,
+ });
+
+ /// Determines which [SharedAxisTransitionType] to build.
+ final SharedAxisTransitionType transitionType;
+
+ /// The color to use for the background color during the transition.
+ ///
+ /// This defaults to the [Theme]'s [ThemeData.canvasColor].
+ final Color? fillColor;
+
+ @override
+ Widget buildTransitions<T>(
+ PageRoute<T>? route,
+ BuildContext? context,
+ Animation<double> animation,
+ Animation<double> secondaryAnimation,
+ Widget child,
+ ) {
+ return SharedAxisTransition(
+ animation: animation,
+ secondaryAnimation: secondaryAnimation,
+ transitionType: transitionType,
+ fillColor: fillColor,
+ child: child,
+ );
+ }
+}
+
+/// Defines a transition in which outgoing and incoming elements share a fade
+/// transition.
+///
+/// The shared axis pattern provides the transition animation between UI elements
+/// that have a spatial or navigational relationship. For example,
+/// transitioning from one page of a sign up page to the next one.
+///
+/// Consider using [SharedAxisTransition] within a
+/// [PageTransitionsTheme] if you want to apply this kind of transition to
+/// [MaterialPageRoute] transitions within a Navigator (see
+/// [SharedAxisPageTransitionsBuilder] for example code).
+///
+/// This transition can also be used directly in a
+/// [PageTransitionSwitcher.transitionBuilder] to transition
+/// from one widget to another as seen in the following example:
+///
+/// ```dart
+/// int _selectedIndex = 0;
+///
+/// final List<Color> _colors = [Colors.white, Colors.red, Colors.yellow];
+///
+/// @override
+/// Widget build(BuildContext context) {
+/// return Scaffold(
+/// appBar: AppBar(
+/// title: const Text('Page Transition Example'),
+/// ),
+/// body: PageTransitionSwitcher(
+/// // reverse: true, // uncomment to see transition in reverse
+/// transitionBuilder: (
+/// Widget child,
+/// Animation<double> primaryAnimation,
+/// Animation<double> secondaryAnimation,
+/// ) {
+/// return SharedAxisTransition(
+/// animation: primaryAnimation,
+/// secondaryAnimation: secondaryAnimation,
+/// transitionType: SharedAxisTransitionType.horizontal,
+/// child: child,
+/// );
+/// },
+/// child: Container(
+/// key: ValueKey<int>(_selectedIndex),
+/// color: _colors[_selectedIndex],
+/// child: Center(
+/// child: FlutterLogo(size: 300),
+/// )
+/// ),
+/// ),
+/// bottomNavigationBar: BottomNavigationBar(
+/// items: const <BottomNavigationBarItem>[
+/// BottomNavigationBarItem(
+/// icon: Icon(Icons.home),
+/// title: Text('White'),
+/// ),
+/// BottomNavigationBarItem(
+/// icon: Icon(Icons.business),
+/// title: Text('Red'),
+/// ),
+/// BottomNavigationBarItem(
+/// icon: Icon(Icons.school),
+/// title: Text('Yellow'),
+/// ),
+/// ],
+/// currentIndex: _selectedIndex,
+/// onTap: (int index) {
+/// setState(() {
+/// _selectedIndex = index;
+/// });
+/// },
+/// ),
+/// );
+/// }
+/// ```
+class SharedAxisTransition extends StatelessWidget {
+ /// Creates a [SharedAxisTransition].
+ ///
+ /// The [animation] and [secondaryAnimation] argument are required and must
+ /// not be null.
+ const SharedAxisTransition({
+ Key? key,
+ required this.animation,
+ required this.secondaryAnimation,
+ required this.transitionType,
+ this.fillColor,
+ this.child,
+ }) : super(key: key);
+
+ /// The animation that drives the [child]'s entrance and exit.
+ ///
+ /// See also:
+ ///
+ /// * [TransitionRoute.animate], which is the value given to this property
+ /// when it is used as a page transition.
+ final Animation<double> animation;
+
+ /// The animation that transitions [child] when new content is pushed on top
+ /// of it.
+ ///
+ /// See also:
+ ///
+ /// * [TransitionRoute.secondaryAnimation], which is the value given to this
+ /// property when the it is used as a page transition.
+ final Animation<double> secondaryAnimation;
+
+ /// Determines which type of shared axis transition is used.
+ ///
+ /// See also:
+ ///
+ /// * [SharedAxisTransitionType], which defines and describes all shared
+ /// axis transition types.
+ final SharedAxisTransitionType transitionType;
+
+ /// The color to use for the background color during the transition.
+ ///
+ /// This defaults to the [Theme]'s [ThemeData.canvasColor].
+ final Color? fillColor;
+
+ /// The widget below this widget in the tree.
+ ///
+ /// This widget will transition in and out as driven by [animation] and
+ /// [secondaryAnimation].
+ final Widget? child;
+
+ @override
+ Widget build(BuildContext context) {
+ final Color color = fillColor ?? Theme.of(context).canvasColor;
+ return DualTransitionBuilder(
+ animation: animation,
+ forwardBuilder: (
+ BuildContext context,
+ Animation<double> animation,
+ Widget? child,
+ ) {
+ return _EnterTransition(
+ animation: animation,
+ transitionType: transitionType,
+ child: child,
+ );
+ },
+ reverseBuilder: (
+ BuildContext context,
+ Animation<double> animation,
+ Widget? child,
+ ) {
+ return _ExitTransition(
+ animation: animation,
+ transitionType: transitionType,
+ reverse: true,
+ fillColor: color,
+ child: child,
+ );
+ },
+ child: DualTransitionBuilder(
+ animation: ReverseAnimation(secondaryAnimation),
+ forwardBuilder: (
+ BuildContext context,
+ Animation<double> animation,
+ Widget? child,
+ ) {
+ return _EnterTransition(
+ animation: animation,
+ transitionType: transitionType,
+ reverse: true,
+ child: child,
+ );
+ },
+ reverseBuilder: (
+ BuildContext context,
+ Animation<double> animation,
+ Widget? child,
+ ) {
+ return _ExitTransition(
+ animation: animation,
+ transitionType: transitionType,
+ fillColor: color,
+ child: child,
+ );
+ },
+ child: child,
+ ),
+ );
+ }
+}
+
+class _EnterTransition extends StatelessWidget {
+ const _EnterTransition({
+ required this.animation,
+ required this.transitionType,
+ this.reverse = false,
+ this.child,
+ });
+
+ final Animation<double> animation;
+ final SharedAxisTransitionType transitionType;
+ final Widget? child;
+ final bool reverse;
+
+ static final Animatable<double> _fadeInTransition = CurveTween(
+ curve: decelerateEasing,
+ ).chain(CurveTween(curve: const Interval(0.3, 1.0)));
+
+ static final Animatable<double> _scaleDownTransition = Tween<double>(
+ begin: 1.10,
+ end: 1.00,
+ ).chain(CurveTween(curve: standardEasing));
+
+ static final Animatable<double> _scaleUpTransition = Tween<double>(
+ begin: 0.80,
+ end: 1.00,
+ ).chain(CurveTween(curve: standardEasing));
+
+ @override
+ Widget build(BuildContext context) {
+ switch (transitionType) {
+ case SharedAxisTransitionType.horizontal:
+ final Animatable<Offset> slideInTransition = Tween<Offset>(
+ begin: Offset(!reverse ? 30.0 : -30.0, 0.0),
+ end: Offset.zero,
+ ).chain(CurveTween(curve: standardEasing));
+
+ return FadeTransition(
+ opacity: _fadeInTransition.animate(animation),
+ child: AnimatedBuilder(
+ animation: animation,
+ builder: (BuildContext context, Widget? child) {
+ return Transform.translate(
+ offset: slideInTransition.evaluate(animation),
+ child: child,
+ );
+ },
+ child: child,
+ ),
+ );
+ case SharedAxisTransitionType.vertical:
+ final Animatable<Offset> slideInTransition = Tween<Offset>(
+ begin: Offset(0.0, !reverse ? 30.0 : -30.0),
+ end: Offset.zero,
+ ).chain(CurveTween(curve: standardEasing));
+
+ return FadeTransition(
+ opacity: _fadeInTransition.animate(animation),
+ child: AnimatedBuilder(
+ animation: animation,
+ builder: (BuildContext context, Widget? child) {
+ return Transform.translate(
+ offset: slideInTransition.evaluate(animation),
+ child: child,
+ );
+ },
+ child: child,
+ ),
+ );
+ case SharedAxisTransitionType.scaled:
+ return FadeTransition(
+ opacity: _fadeInTransition.animate(animation),
+ child: ScaleTransition(
+ scale: (!reverse ? _scaleUpTransition : _scaleDownTransition)
+ .animate(animation),
+ child: child,
+ ),
+ );
+ }
+ }
+}
+
+class _ExitTransition extends StatelessWidget {
+ const _ExitTransition({
+ required this.animation,
+ required this.transitionType,
+ this.reverse = false,
+ required this.fillColor,
+ this.child,
+ });
+
+ final Animation<double> animation;
+ final SharedAxisTransitionType transitionType;
+ final bool reverse;
+ final Color fillColor;
+ final Widget? child;
+
+ static final Animatable<double> _fadeOutTransition = _FlippedCurveTween(
+ curve: accelerateEasing,
+ ).chain(CurveTween(curve: const Interval(0.0, 0.3)));
+
+ static final Animatable<double> _scaleUpTransition = Tween<double>(
+ begin: 1.00,
+ end: 1.10,
+ ).chain(CurveTween(curve: standardEasing));
+
+ static final Animatable<double> _scaleDownTransition = Tween<double>(
+ begin: 1.00,
+ end: 0.80,
+ ).chain(CurveTween(curve: standardEasing));
+
+ @override
+ Widget build(BuildContext context) {
+ switch (transitionType) {
+ case SharedAxisTransitionType.horizontal:
+ final Animatable<Offset> slideOutTransition = Tween<Offset>(
+ begin: Offset.zero,
+ end: Offset(!reverse ? -30.0 : 30.0, 0.0),
+ ).chain(CurveTween(curve: standardEasing));
+
+ return FadeTransition(
+ opacity: _fadeOutTransition.animate(animation),
+ child: Container(
+ color: fillColor,
+ child: AnimatedBuilder(
+ animation: animation,
+ builder: (BuildContext context, Widget? child) {
+ return Transform.translate(
+ offset: slideOutTransition.evaluate(animation),
+ child: child,
+ );
+ },
+ child: child,
+ ),
+ ),
+ );
+ case SharedAxisTransitionType.vertical:
+ final Animatable<Offset> slideOutTransition = Tween<Offset>(
+ begin: Offset.zero,
+ end: Offset(0.0, !reverse ? -30.0 : 30.0),
+ ).chain(CurveTween(curve: standardEasing));
+
+ return FadeTransition(
+ opacity: _fadeOutTransition.animate(animation),
+ child: Container(
+ color: fillColor,
+ child: AnimatedBuilder(
+ animation: animation,
+ builder: (BuildContext context, Widget? child) {
+ return Transform.translate(
+ offset: slideOutTransition.evaluate(animation),
+ child: child,
+ );
+ },
+ child: child,
+ ),
+ ),
+ );
+ case SharedAxisTransitionType.scaled:
+ return FadeTransition(
+ opacity: _fadeOutTransition.animate(animation),
+ child: Container(
+ color: fillColor,
+ child: ScaleTransition(
+ scale: (!reverse ? _scaleUpTransition : _scaleDownTransition)
+ .animate(animation),
+ child: child,
+ ),
+ ),
+ );
+ }
+ }
+}
+
+/// Enables creating a flipped [CurveTween].
+///
+/// This creates a [CurveTween] that evaluates to a result that flips the
+/// tween vertically.
+///
+/// This tween sequence assumes that the evaluated result has to be a double
+/// between 0.0 and 1.0.
+class _FlippedCurveTween extends CurveTween {
+ /// Creates a vertically flipped [CurveTween].
+ _FlippedCurveTween({
+ required Curve curve,
+ }) : super(curve: curve);
+
+ @override
+ double transform(double t) => 1.0 - super.transform(t);
+}
diff --git a/packages/animations/pubspec.yaml b/packages/animations/pubspec.yaml
new file mode 100644
index 0000000..81150b1
--- /dev/null
+++ b/packages/animations/pubspec.yaml
@@ -0,0 +1,16 @@
+name: animations
+description: Fancy pre-built animations that can easily be integrated into any Flutter application.
+version: 2.0.0
+homepage: https://github.com/flutter/packages/tree/master/packages/animations
+
+environment:
+ sdk: '>=2.12.0-259.9.beta <3.0.0'
+
+dependencies:
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+ vector_math: ^2.1.0
diff --git a/packages/animations/test/dual_transition_builder_test.dart b/packages/animations/test/dual_transition_builder_test.dart
new file mode 100644
index 0000000..09a80af
--- /dev/null
+++ b/packages/animations/test/dual_transition_builder_test.dart
@@ -0,0 +1,298 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/widgets.dart';
+
+void main() {
+ testWidgets('runs animations', (WidgetTester tester) async {
+ final AnimationController controller = AnimationController(
+ vsync: const TestVSync(),
+ duration: const Duration(milliseconds: 300),
+ );
+
+ await tester.pumpWidget(Center(
+ child: DualTransitionBuilder(
+ animation: controller,
+ forwardBuilder: (
+ BuildContext context,
+ Animation<double> animation,
+ Widget? child,
+ ) {
+ return ScaleTransition(
+ scale: animation,
+ child: child,
+ );
+ },
+ reverseBuilder: (
+ BuildContext context,
+ Animation<double> animation,
+ Widget? child,
+ ) {
+ return FadeTransition(
+ opacity: Tween<double>(begin: 1.0, end: 0.0).animate(animation),
+ child: child,
+ );
+ },
+ child: Container(
+ color: Colors.green,
+ height: 100,
+ width: 100,
+ ),
+ ),
+ ));
+ expect(_getScale(tester), 0.0);
+ expect(_getOpacity(tester), 1.0);
+
+ controller.forward();
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(_getScale(tester), 0.5);
+ expect(_getOpacity(tester), 1.0);
+
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(_getScale(tester), 1.0);
+ expect(_getOpacity(tester), 1.0);
+
+ await tester.pumpAndSettle();
+ expect(_getScale(tester), 1.0);
+ expect(_getOpacity(tester), 1.0);
+
+ controller.reverse();
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(_getScale(tester), 1.0);
+ expect(_getOpacity(tester), 0.5);
+
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(_getScale(tester), 1.0);
+ expect(_getOpacity(tester), 0.0);
+
+ await tester.pumpAndSettle();
+ expect(_getScale(tester), 0.0);
+ expect(_getOpacity(tester), 1.0);
+ });
+
+ testWidgets('keeps state', (WidgetTester tester) async {
+ final AnimationController controller = AnimationController(
+ vsync: const TestVSync(),
+ duration: const Duration(milliseconds: 300),
+ );
+
+ await tester.pumpWidget(Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: DualTransitionBuilder(
+ animation: controller,
+ forwardBuilder: (
+ BuildContext context,
+ Animation<double> animation,
+ Widget? child,
+ ) {
+ return ScaleTransition(
+ scale: animation,
+ child: child,
+ );
+ },
+ reverseBuilder: (
+ BuildContext context,
+ Animation<double> animation,
+ Widget? child,
+ ) {
+ return FadeTransition(
+ opacity: Tween<double>(begin: 1.0, end: 0.0).animate(animation),
+ child: child,
+ );
+ },
+ child: const _StatefulTestWidget(name: 'Foo'),
+ ),
+ ),
+ ));
+ final State<StatefulWidget> state =
+ tester.state(find.byType(_StatefulTestWidget));
+ expect(state, isNotNull);
+
+ controller.forward();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ controller.reverse();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ });
+
+ testWidgets('does not jump when interrupted - forward',
+ (WidgetTester tester) async {
+ final AnimationController controller = AnimationController(
+ vsync: const TestVSync(),
+ duration: const Duration(milliseconds: 300),
+ );
+ await tester.pumpWidget(Center(
+ child: DualTransitionBuilder(
+ animation: controller,
+ forwardBuilder: (
+ BuildContext context,
+ Animation<double> animation,
+ Widget? child,
+ ) {
+ return ScaleTransition(
+ scale: animation,
+ child: child,
+ );
+ },
+ reverseBuilder: (
+ BuildContext context,
+ Animation<double> animation,
+ Widget? child,
+ ) {
+ return FadeTransition(
+ opacity: Tween<double>(begin: 1.0, end: 0.0).animate(animation),
+ child: child,
+ );
+ },
+ child: Container(
+ color: Colors.green,
+ height: 100,
+ width: 100,
+ ),
+ ),
+ ));
+ expect(_getScale(tester), 0.0);
+ expect(_getOpacity(tester), 1.0);
+
+ controller.forward();
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(_getScale(tester), 0.5);
+ expect(_getOpacity(tester), 1.0);
+
+ controller.reverse();
+ expect(_getScale(tester), 0.5);
+ expect(_getOpacity(tester), 1.0);
+ await tester.pump();
+ expect(_getScale(tester), 0.5);
+ expect(_getOpacity(tester), 1.0);
+
+ await tester.pump(const Duration(milliseconds: 75));
+ expect(_getScale(tester), 0.25);
+ expect(_getOpacity(tester), 1.0);
+
+ await tester.pump(const Duration(milliseconds: 75));
+ expect(_getScale(tester), 0.0);
+ expect(_getOpacity(tester), 1.0);
+
+ await tester.pumpAndSettle();
+ expect(_getScale(tester), 0.0);
+ expect(_getOpacity(tester), 1.0);
+ });
+
+ testWidgets('does not jump when interrupted - reverse',
+ (WidgetTester tester) async {
+ final AnimationController controller = AnimationController(
+ value: 1.0,
+ vsync: const TestVSync(),
+ duration: const Duration(milliseconds: 300),
+ );
+ await tester.pumpWidget(Center(
+ child: DualTransitionBuilder(
+ animation: controller,
+ forwardBuilder: (
+ BuildContext context,
+ Animation<double> animation,
+ Widget? child,
+ ) {
+ return ScaleTransition(
+ scale: animation,
+ child: child,
+ );
+ },
+ reverseBuilder: (
+ BuildContext context,
+ Animation<double> animation,
+ Widget? child,
+ ) {
+ return FadeTransition(
+ opacity: Tween<double>(begin: 1.0, end: 0.0).animate(animation),
+ child: child,
+ );
+ },
+ child: Container(
+ color: Colors.green,
+ height: 100,
+ width: 100,
+ ),
+ ),
+ ));
+ expect(_getScale(tester), 1.0);
+ expect(_getOpacity(tester), 1.0);
+
+ controller.reverse();
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(_getScale(tester), 1.0);
+ expect(_getOpacity(tester), 0.5);
+
+ controller.forward();
+ expect(_getScale(tester), 1.0);
+ expect(_getOpacity(tester), 0.5);
+ await tester.pump();
+ expect(_getScale(tester), 1.0);
+ expect(_getOpacity(tester), 0.5);
+
+ await tester.pump(const Duration(milliseconds: 75));
+ expect(_getScale(tester), 1.0);
+ expect(_getOpacity(tester), 0.75);
+
+ await tester.pump(const Duration(milliseconds: 75));
+ expect(_getScale(tester), 1.0);
+ expect(_getOpacity(tester), 1.0);
+
+ await tester.pumpAndSettle();
+ expect(_getScale(tester), 1.0);
+ expect(_getOpacity(tester), 1.0);
+ });
+}
+
+double _getScale(WidgetTester tester) {
+ final ScaleTransition scale = tester.widget(find.byType(ScaleTransition));
+ return scale.scale.value;
+}
+
+double _getOpacity(WidgetTester tester) {
+ final FadeTransition scale = tester.widget(find.byType(FadeTransition));
+ return scale.opacity.value;
+}
+
+class _StatefulTestWidget extends StatefulWidget {
+ const _StatefulTestWidget({Key? key, required this.name}) : super(key: key);
+
+ final String name;
+
+ @override
+ State<_StatefulTestWidget> createState() => _StatefulTestWidgetState();
+}
+
+class _StatefulTestWidgetState extends State<_StatefulTestWidget> {
+ @override
+ Widget build(BuildContext context) {
+ return Text(widget.name);
+ }
+}
diff --git a/packages/animations/test/fade_scale_transition_test.dart b/packages/animations/test/fade_scale_transition_test.dart
new file mode 100644
index 0000000..f87d182
--- /dev/null
+++ b/packages/animations/test/fade_scale_transition_test.dart
@@ -0,0 +1,501 @@
+// 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:animations/src/fade_scale_transition.dart';
+import 'package:animations/src/modal.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/widgets.dart';
+
+void main() {
+ testWidgets(
+ 'FadeScaleTransitionConfiguration builds a new route',
+ (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Builder(builder: (BuildContext context) {
+ return Center(
+ child: ElevatedButton(
+ onPressed: () {
+ showModal<void>(
+ context: context,
+ configuration: const FadeScaleTransitionConfiguration(),
+ builder: (BuildContext context) {
+ return const _FlutterLogoModal();
+ },
+ );
+ },
+ child: const Icon(Icons.add),
+ ),
+ );
+ }),
+ ),
+ ),
+ );
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+ expect(find.byType(_FlutterLogoModal), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'FadeScaleTransitionConfiguration runs forward',
+ (WidgetTester tester) async {
+ final GlobalKey key = GlobalKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Builder(builder: (BuildContext context) {
+ return Center(
+ child: ElevatedButton(
+ onPressed: () {
+ showModal<void>(
+ context: context,
+ configuration: const FadeScaleTransitionConfiguration(),
+ builder: (BuildContext context) {
+ return _FlutterLogoModal(key: key);
+ },
+ );
+ },
+ child: const Icon(Icons.add),
+ ),
+ );
+ }),
+ ),
+ ),
+ );
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pump();
+ // Opacity duration: First 30% of 150ms, linear transition
+ double topFadeTransitionOpacity = _getOpacity(key, tester);
+ double topScale = _getScale(key, tester);
+ expect(topFadeTransitionOpacity, 0.0);
+ expect(topScale, 0.80);
+
+ // 3/10 * 150ms = 45ms (total opacity animation duration)
+ // 1/2 * 45ms = ~23ms elapsed for halfway point of opacity
+ // animation
+ await tester.pump(const Duration(milliseconds: 23));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, closeTo(0.5, 0.05));
+ topScale = _getScale(key, tester);
+ expect(topScale, greaterThan(0.80));
+ expect(topScale, lessThan(1.0));
+
+ // End of opacity animation
+ await tester.pump(const Duration(milliseconds: 22));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, 1.0);
+ topScale = _getScale(key, tester);
+ expect(topScale, greaterThan(0.80));
+ expect(topScale, lessThan(1.0));
+
+ // 100ms into the animation
+ await tester.pump(const Duration(milliseconds: 55));
+ topScale = _getScale(key, tester);
+ expect(topScale, greaterThan(0.80));
+ expect(topScale, lessThan(1.0));
+
+ // Get to the end of the animation
+ await tester.pump(const Duration(milliseconds: 50));
+ topScale = _getScale(key, tester);
+ expect(topScale, 1.0);
+
+ await tester.pump();
+ expect(find.byType(_FlutterLogoModal), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'FadeScaleTransitionConfiguration runs forward',
+ (WidgetTester tester) async {
+ final GlobalKey key = GlobalKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Builder(builder: (BuildContext context) {
+ return Center(
+ child: ElevatedButton(
+ onPressed: () {
+ showModal<void>(
+ context: context,
+ configuration: const FadeScaleTransitionConfiguration(),
+ builder: (BuildContext context) {
+ return _FlutterLogoModal(key: key);
+ },
+ );
+ },
+ child: const Icon(Icons.add),
+ ),
+ );
+ }),
+ ),
+ ),
+ );
+ // Show the incoming modal and let it animate in fully.
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+
+ // Tap on modal barrier to start reverse animation.
+ await tester.tapAt(Offset.zero);
+ await tester.pump();
+
+ // Opacity duration: Linear transition throughout 75ms
+ // No scale animations on exit transition.
+ double topFadeTransitionOpacity = _getOpacity(key, tester);
+ double topScale = _getScale(key, tester);
+ expect(topFadeTransitionOpacity, 1.0);
+ expect(topScale, 1.0);
+
+ await tester.pump(const Duration(milliseconds: 25));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ topScale = _getScale(key, tester);
+ expect(topFadeTransitionOpacity, closeTo(0.66, 0.05));
+ expect(topScale, 1.0);
+
+ await tester.pump(const Duration(milliseconds: 25));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ topScale = _getScale(key, tester);
+ expect(topFadeTransitionOpacity, closeTo(0.33, 0.05));
+ expect(topScale, 1.0);
+
+ // End of opacity animation
+ await tester.pump(const Duration(milliseconds: 25));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, 0.0);
+ topScale = _getScale(key, tester);
+ expect(topScale, 1.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.byType(_FlutterLogoModal), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'FadeScaleTransitionConfiguration does not jump when interrupted',
+ (WidgetTester tester) async {
+ final GlobalKey key = GlobalKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Builder(builder: (BuildContext context) {
+ return Center(
+ child: ElevatedButton(
+ onPressed: () {
+ showModal<void>(
+ context: context,
+ configuration: const FadeScaleTransitionConfiguration(),
+ builder: (BuildContext context) {
+ return _FlutterLogoModal(key: key);
+ },
+ );
+ },
+ child: const Icon(Icons.add),
+ ),
+ );
+ }),
+ ),
+ ),
+ );
+
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pump();
+ // Opacity duration: First 30% of 150ms, linear transition
+ double topFadeTransitionOpacity = _getOpacity(key, tester);
+ double topScale = _getScale(key, tester);
+ expect(topFadeTransitionOpacity, 0.0);
+ expect(topScale, 0.80);
+
+ // 3/10 * 150ms = 45ms (total opacity animation duration)
+ // End of opacity animation
+ await tester.pump(const Duration(milliseconds: 45));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, 1.0);
+ topScale = _getScale(key, tester);
+ expect(topScale, greaterThan(0.80));
+ expect(topScale, lessThan(1.0));
+
+ // 100ms into the animation
+ await tester.pump(const Duration(milliseconds: 55));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, 1.0);
+ topScale = _getScale(key, tester);
+ expect(topScale, greaterThan(0.80));
+ expect(topScale, lessThan(1.0));
+
+ // Start the reverse transition by interrupting the forwards
+ // transition.
+ await tester.tapAt(Offset.zero);
+ await tester.pump();
+ // Opacity and scale values should remain the same after
+ // the reverse animation starts.
+ expect(_getOpacity(key, tester), topFadeTransitionOpacity);
+ expect(_getScale(key, tester), topScale);
+
+ // Should animate in reverse with 2/3 * 75ms = 50ms
+ // using the enter transition's animation pattern
+ // instead of the exit animation pattern.
+
+ // Calculation for the time when the linear fade
+ // transition should start if running backwards:
+ // 3/10 * 75ms = 22.5ms
+ // To get the 22.5ms timestamp, run backwards for:
+ // 50ms - 22.5ms = ~27.5ms
+ await tester.pump(const Duration(milliseconds: 27));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, 1.0);
+ topScale = _getScale(key, tester);
+ expect(topScale, greaterThan(0.80));
+ expect(topScale, lessThan(1.0));
+
+ // Halfway through fade animation
+ await tester.pump(const Duration(milliseconds: 12));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, closeTo(0.5, 0.05));
+ topScale = _getScale(key, tester);
+ expect(topScale, greaterThan(0.80));
+ expect(topScale, lessThan(1.0));
+
+ // Complete the rest of the animation
+ await tester.pump(const Duration(milliseconds: 11));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, 0.0);
+ topScale = _getScale(key, tester);
+ expect(topScale, 0.8);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.byType(_FlutterLogoModal), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'State is not lost when transitioning',
+ (WidgetTester tester) async {
+ final GlobalKey bottomKey = GlobalKey();
+ final GlobalKey topKey = GlobalKey();
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Builder(builder: (BuildContext context) {
+ return Center(
+ child: Column(
+ children: <Widget>[
+ ElevatedButton(
+ onPressed: () {
+ showModal<void>(
+ context: context,
+ configuration:
+ const FadeScaleTransitionConfiguration(),
+ builder: (BuildContext context) {
+ return _FlutterLogoModal(
+ key: topKey,
+ name: 'top route',
+ );
+ },
+ );
+ },
+ child: const Icon(Icons.add),
+ ),
+ _FlutterLogoModal(
+ key: bottomKey,
+ name: 'bottom route',
+ ),
+ ],
+ ),
+ );
+ }),
+ ),
+ ),
+ );
+
+ // The bottom route's state should already exist.
+ final _FlutterLogoModalState bottomState = tester.state(
+ find.byKey(bottomKey),
+ );
+ expect(bottomState.widget.name, 'bottom route');
+
+ // Start the enter transition of the modal route.
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pump();
+ await tester.pump();
+
+ // The bottom route's state should be retained at the start of the
+ // transition.
+ expect(
+ tester.state(find.byKey(bottomKey)),
+ bottomState,
+ );
+ // The top route's state should be created.
+ final _FlutterLogoModalState topState = tester.state(
+ find.byKey(topKey),
+ );
+ expect(topState.widget.name, 'top route');
+
+ // Halfway point of forwards animation.
+ await tester.pump(const Duration(milliseconds: 75));
+ expect(
+ tester.state(find.byKey(bottomKey)),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(topKey)),
+ topState,
+ );
+
+ // End the transition and see if top and bottom routes'
+ // states persist.
+ await tester.pumpAndSettle();
+ expect(
+ tester.state(find.byKey(
+ bottomKey,
+ skipOffstage: false,
+ )),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(topKey)),
+ topState,
+ );
+
+ // Start the reverse animation. Both top and bottom
+ // routes' states should persist.
+ await tester.tapAt(Offset.zero);
+ await tester.pump();
+ expect(
+ tester.state(find.byKey(bottomKey)),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(topKey)),
+ topState,
+ );
+
+ // Halfway point of the exit transition.
+ await tester.pump(const Duration(milliseconds: 38));
+ expect(
+ tester.state(find.byKey(bottomKey)),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(topKey)),
+ topState,
+ );
+
+ // End the exit transition. The bottom route's state should
+ // persist, whereas the top route's state should no longer
+ // be present.
+ await tester.pumpAndSettle();
+ expect(
+ tester.state(find.byKey(bottomKey)),
+ bottomState,
+ );
+ expect(find.byKey(topKey), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'should preserve state',
+ (WidgetTester tester) async {
+ final AnimationController controller = AnimationController(
+ vsync: const TestVSync(),
+ duration: const Duration(milliseconds: 300),
+ );
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Center(
+ child: FadeScaleTransition(
+ animation: controller,
+ child: const _FlutterLogoModal(),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final State<StatefulWidget> state = tester.state(
+ find.byType(_FlutterLogoModal),
+ );
+ expect(state, isNotNull);
+
+ controller.forward();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+
+ controller.reverse();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+
+ controller.forward();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+ },
+ );
+}
+
+double _getOpacity(GlobalKey key, WidgetTester tester) {
+ final Finder finder = find.ancestor(
+ of: find.byKey(key),
+ matching: find.byType(FadeTransition),
+ );
+ return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
+ final FadeTransition transition = widget as FadeTransition;
+ return a * transition.opacity.value;
+ });
+}
+
+double _getScale(GlobalKey key, WidgetTester tester) {
+ final Finder finder = find.ancestor(
+ of: find.byKey(key),
+ matching: find.byType(ScaleTransition),
+ );
+ return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
+ final ScaleTransition transition = widget as ScaleTransition;
+ return a * transition.scale.value;
+ });
+}
+
+class _FlutterLogoModal extends StatefulWidget {
+ const _FlutterLogoModal({
+ Key? key,
+ this.name,
+ }) : super(key: key);
+
+ final String? name;
+
+ @override
+ _FlutterLogoModalState createState() => _FlutterLogoModalState();
+}
+
+class _FlutterLogoModalState extends State<_FlutterLogoModal> {
+ @override
+ Widget build(BuildContext context) {
+ return const Center(
+ child: SizedBox(
+ width: 250,
+ height: 250,
+ child: Material(
+ child: Center(
+ child: FlutterLogo(size: 250),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/packages/animations/test/fade_through_transition_test.dart b/packages/animations/test/fade_through_transition_test.dart
new file mode 100644
index 0000000..ac0ba19
--- /dev/null
+++ b/packages/animations/test/fade_through_transition_test.dart
@@ -0,0 +1,507 @@
+// 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:animations/src/fade_through_transition.dart';
+import 'package:flutter/animation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/widgets.dart';
+
+void main() {
+ testWidgets(
+ 'FadeThroughPageTransitionsBuilder builds a FadeThroughTransition',
+ (WidgetTester tester) async {
+ final AnimationController animation = AnimationController(
+ vsync: const TestVSync(),
+ );
+ final AnimationController secondaryAnimation = AnimationController(
+ vsync: const TestVSync(),
+ );
+
+ await tester.pumpWidget(
+ const FadeThroughPageTransitionsBuilder().buildTransitions<void>(
+ null,
+ null,
+ animation,
+ secondaryAnimation,
+ const Placeholder(),
+ ));
+
+ expect(find.byType(FadeThroughTransition), findsOneWidget);
+ });
+
+ testWidgets('FadeThroughTransition runs forward',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ ),
+ );
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.0);
+ expect(_getOpacity(bottomRoute, tester), 1.0);
+ expect(find.text(topRoute), findsNothing);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+ await tester.pump();
+
+ // Bottom route is full size and fully visible.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.0);
+ expect(_getOpacity(bottomRoute, tester), 1.0);
+ // top route is at 95% of full size and not visible yet.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 0.92);
+ expect(_getOpacity(topRoute, tester), 0.0);
+
+ // Jump to half-way through the fade out (total duration is 300ms, 6/12th of
+ // that are fade out, so half-way is 300 * 6/12 / 2 = 45ms.
+ await tester.pump(const Duration(milliseconds: 45));
+ // Bottom route is fading out.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.0);
+ final double bottomOpacity = _getOpacity(bottomRoute, tester);
+ expect(bottomOpacity, lessThan(1.0));
+ expect(bottomOpacity, greaterThan(0.0));
+ // Top route is still invisible.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 0.92);
+ expect(_getOpacity(topRoute, tester), 0.0);
+
+ // Let's jump to the end of the fade-out.
+ await tester.pump(const Duration(milliseconds: 45));
+ // Bottom route is completely faded out.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.0);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ // Top route is still invisible.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 0.92);
+ expect(_getOpacity(topRoute, tester), 0.0);
+
+ // Let's jump to the middle of the fade-in.
+ await tester.pump(const Duration(milliseconds: 105));
+ // Bottom route is not visible.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.0);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ // Top route is fading/scaling in.
+ expect(find.text(topRoute), findsOneWidget);
+ final double topScale = _getScale(topRoute, tester);
+ final double topOpacity = _getOpacity(topRoute, tester);
+ expect(topScale, greaterThan(0.92));
+ expect(topScale, lessThan(1.0));
+ expect(topOpacity, greaterThan(0.0));
+ expect(topOpacity, lessThan(1.0));
+
+ // Let's jump to the end of the animation.
+ await tester.pump(const Duration(milliseconds: 105));
+ // Bottom route is not visible.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.0);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ // Top route fully scaled in and visible.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 1.0);
+ expect(_getOpacity(topRoute, tester), 1.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text(bottomRoute), findsNothing);
+ expect(find.text(topRoute), findsOneWidget);
+ });
+
+ testWidgets('FadeThroughTransition runs backwards',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ ),
+ );
+ navigator.currentState!.pushNamed('/a');
+ await tester.pumpAndSettle();
+
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 1.0);
+ expect(_getOpacity(topRoute, tester), 1.0);
+ expect(find.text(bottomRoute), findsNothing);
+
+ navigator.currentState!.pop();
+ await tester.pump();
+
+ // Top route is full size and fully visible.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 1.0);
+ expect(_getOpacity(topRoute, tester), 1.0);
+ // Bottom route is at 95% of full size and not visible yet.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 0.92);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+
+ // Jump to half-way through the fade out (total duration is 300ms, 6/12th of
+ // that are fade out, so half-way is 300 * 6/12 / 2 = 45ms.
+ await tester.pump(const Duration(milliseconds: 45));
+ // Bottom route is fading out.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 1.0);
+ final double topOpacity = _getOpacity(topRoute, tester);
+ expect(topOpacity, lessThan(1.0));
+ expect(topOpacity, greaterThan(0.0));
+ // Top route is still invisible.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 0.92);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+
+ // Let's jump to the end of the fade-out.
+ await tester.pump(const Duration(milliseconds: 45));
+ // Bottom route is completely faded out.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 1.0);
+ expect(_getOpacity(topRoute, tester), 0.0);
+ // Top route is still invisible.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getScale(bottomRoute, tester),
+ moreOrLessEquals(0.92, epsilon: 0.005),
+ );
+ expect(
+ _getOpacity(bottomRoute, tester),
+ moreOrLessEquals(0.0, epsilon: 0.005),
+ );
+
+ // Let's jump to the middle of the fade-in.
+ await tester.pump(const Duration(milliseconds: 105));
+ // Bottom route is not visible.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 1.0);
+ expect(_getOpacity(topRoute, tester), 0.0);
+ // Top route is fading/scaling in.
+ expect(find.text(bottomRoute), findsOneWidget);
+ final double bottomScale = _getScale(bottomRoute, tester);
+ final double bottomOpacity = _getOpacity(bottomRoute, tester);
+ expect(bottomScale, greaterThan(0.96));
+ expect(bottomScale, lessThan(1.0));
+ expect(bottomOpacity, greaterThan(0.1));
+ expect(bottomOpacity, lessThan(1.0));
+
+ // Let's jump to the end of the animation.
+ await tester.pump(const Duration(milliseconds: 105));
+ // Bottom route is not visible.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 1.0);
+ expect(_getOpacity(topRoute, tester), 0.0);
+ // Top route fully scaled in and visible.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.0);
+ expect(_getOpacity(bottomRoute, tester), 1.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text(topRoute), findsNothing);
+ expect(find.text(bottomRoute), findsOneWidget);
+ });
+
+ testWidgets('FadeThroughTransition does not jump when interrupted',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ ),
+ );
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(find.text(topRoute), findsNothing);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+
+ // Jump to halfway point of transition.
+ await tester.pump(const Duration(milliseconds: 150));
+ // Bottom route is fully faded out.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.0);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ // Top route is fading/scaling in.
+ expect(find.text(topRoute), findsOneWidget);
+ final double topScale = _getScale(topRoute, tester);
+ final double topOpacity = _getOpacity(topRoute, tester);
+ expect(topScale, greaterThan(0.92));
+ expect(topScale, lessThan(1.0));
+ expect(topOpacity, greaterThan(0.0));
+ expect(topOpacity, lessThan(1.0));
+
+ // Interrupt the transition with a pop.
+ navigator.currentState!.pop();
+ await tester.pump();
+ // Noting changed.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.0);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), topScale);
+ expect(_getOpacity(topRoute, tester), topOpacity);
+
+ // Jump to the halfway point.
+ await tester.pump(const Duration(milliseconds: 75));
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.0);
+ final double bottomOpacity = _getOpacity(bottomRoute, tester);
+ expect(bottomOpacity, greaterThan(0.0));
+ expect(bottomOpacity, lessThan(1.0));
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), lessThan(topScale));
+ expect(_getOpacity(topRoute, tester), lessThan(topOpacity));
+
+ // Jump to the end.
+ await tester.pump(const Duration(milliseconds: 75));
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.0);
+ expect(_getOpacity(bottomRoute, tester), 1.0);
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 0.92);
+ expect(_getOpacity(topRoute, tester), 0.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text(topRoute), findsNothing);
+ expect(find.text(bottomRoute), findsOneWidget);
+ });
+
+ testWidgets('State is not lost when transitioning',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ contentBuilder: (RouteSettings settings) {
+ return _StatefulTestWidget(
+ key: ValueKey<String?>(settings.name),
+ name: settings.name,
+ );
+ },
+ ),
+ );
+
+ final _StatefulTestWidgetState bottomState =
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute)));
+ expect(bottomState.widget.name, bottomRoute);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+ await tester.pump();
+
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ final _StatefulTestWidgetState topState = tester.state(
+ find.byKey(const ValueKey<String?>(topRoute)),
+ );
+ expect(topState.widget.name, topRoute);
+
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+ topState,
+ );
+
+ await tester.pumpAndSettle();
+ expect(
+ tester.state(find.byKey(
+ const ValueKey<String?>(bottomRoute),
+ skipOffstage: false,
+ )),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+ topState,
+ );
+
+ navigator.currentState!.pop();
+ await tester.pump();
+
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+ topState,
+ );
+
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+ topState,
+ );
+
+ await tester.pumpAndSettle();
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ expect(find.byKey(const ValueKey<String?>(topRoute)), findsNothing);
+ });
+
+ testWidgets('should keep state', (WidgetTester tester) async {
+ final AnimationController animation = AnimationController(
+ vsync: const TestVSync(),
+ duration: const Duration(milliseconds: 300),
+ );
+ final AnimationController secondaryAnimation = AnimationController(
+ vsync: const TestVSync(),
+ duration: const Duration(milliseconds: 300),
+ );
+ await tester.pumpWidget(Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: FadeThroughTransition(
+ child: const _StatefulTestWidget(name: 'Foo'),
+ animation: animation,
+ secondaryAnimation: secondaryAnimation,
+ ),
+ ),
+ ));
+ final State<StatefulWidget> state = tester.state(
+ find.byType(_StatefulTestWidget),
+ );
+ expect(state, isNotNull);
+
+ animation.forward();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ secondaryAnimation.forward();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ secondaryAnimation.reverse();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ animation.reverse();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ });
+}
+
+double _getOpacity(String key, WidgetTester tester) {
+ final Finder finder = find.ancestor(
+ of: find.byKey(ValueKey<String?>(key)),
+ matching: find.byType(FadeTransition),
+ );
+ return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
+ final FadeTransition transition = widget as FadeTransition;
+ return a * transition.opacity.value;
+ });
+}
+
+double _getScale(String key, WidgetTester tester) {
+ final Finder finder = find.ancestor(
+ of: find.byKey(ValueKey<String?>(key)),
+ matching: find.byType(ScaleTransition),
+ );
+ return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
+ final ScaleTransition transition = widget as ScaleTransition;
+ return a * transition.scale.value;
+ });
+}
+
+class _TestWidget extends StatelessWidget {
+ const _TestWidget({this.navigatorKey, this.contentBuilder});
+
+ final Key? navigatorKey;
+ final _ContentBuilder? contentBuilder;
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ navigatorKey: navigatorKey as GlobalKey<NavigatorState>?,
+ theme: ThemeData(
+ platform: TargetPlatform.android,
+ pageTransitionsTheme: const PageTransitionsTheme(
+ builders: <TargetPlatform, PageTransitionsBuilder>{
+ TargetPlatform.android: FadeThroughPageTransitionsBuilder(),
+ },
+ ),
+ ),
+ onGenerateRoute: (RouteSettings settings) {
+ return MaterialPageRoute<void>(
+ settings: settings,
+ builder: (BuildContext context) {
+ return contentBuilder != null
+ ? contentBuilder!(settings)
+ : Center(
+ key: ValueKey<String?>(settings.name),
+ child: Text(settings.name!),
+ );
+ },
+ );
+ },
+ );
+ }
+}
+
+class _StatefulTestWidget extends StatefulWidget {
+ const _StatefulTestWidget({Key? key, this.name}) : super(key: key);
+
+ final String? name;
+
+ @override
+ State<_StatefulTestWidget> createState() => _StatefulTestWidgetState();
+}
+
+class _StatefulTestWidgetState extends State<_StatefulTestWidget> {
+ @override
+ Widget build(BuildContext context) {
+ return Text(widget.name!);
+ }
+}
+
+typedef _ContentBuilder = Widget Function(RouteSettings settings);
diff --git a/packages/animations/test/modal_test.dart b/packages/animations/test/modal_test.dart
new file mode 100644
index 0000000..7d9c463
--- /dev/null
+++ b/packages/animations/test/modal_test.dart
@@ -0,0 +1,623 @@
+// 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 'dart:ui' as ui;
+
+import 'package:animations/src/modal.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/widgets.dart';
+
+void main() {
+ testWidgets(
+ 'showModal builds a new route with specified barrier properties',
+ (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Builder(builder: (BuildContext context) {
+ return Center(
+ child: ElevatedButton(
+ onPressed: () {
+ showModal<void>(
+ context: context,
+ configuration: _TestModalConfiguration(),
+ builder: (BuildContext context) {
+ return const _FlutterLogoModal();
+ },
+ );
+ },
+ child: const Icon(Icons.add),
+ ),
+ );
+ }),
+ ),
+ ),
+ );
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+
+ // New route containing _FlutterLogoModal is present.
+ expect(find.byType(_FlutterLogoModal), findsOneWidget);
+ final ModalBarrier topModalBarrier = tester.widget<ModalBarrier>(
+ find.byType(ModalBarrier).at(1),
+ );
+
+ // Verify new route's modal barrier properties are correct.
+ expect(topModalBarrier.color, Colors.green);
+ expect(topModalBarrier.barrierSemanticsDismissible, true);
+ expect(topModalBarrier.semanticsLabel, 'customLabel');
+ },
+ );
+
+ testWidgets(
+ 'showModal forwards animation',
+ (WidgetTester tester) async {
+ final GlobalKey key = GlobalKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Builder(builder: (BuildContext context) {
+ return Center(
+ child: ElevatedButton(
+ onPressed: () {
+ showModal<void>(
+ context: context,
+ configuration: _TestModalConfiguration(),
+ builder: (BuildContext context) {
+ return _FlutterLogoModal(key: key);
+ },
+ );
+ },
+ child: const Icon(Icons.add),
+ ),
+ );
+ }),
+ ),
+ ),
+ );
+
+ // Start forwards animation
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pump();
+
+ // Opacity duration: Linear transition throughout 300ms
+ double topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, 0.0);
+
+ // Halfway through forwards animation.
+ await tester.pump(const Duration(milliseconds: 150));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, 0.5);
+
+ // The end of the transition.
+ await tester.pump(const Duration(milliseconds: 150));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, 1.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.byType(_FlutterLogoModal), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'showModal reverse animation',
+ (WidgetTester tester) async {
+ final GlobalKey key = GlobalKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Builder(builder: (BuildContext context) {
+ return Center(
+ child: ElevatedButton(
+ onPressed: () {
+ showModal<void>(
+ context: context,
+ configuration: _TestModalConfiguration(),
+ builder: (BuildContext context) {
+ return _FlutterLogoModal(key: key);
+ },
+ );
+ },
+ child: const Icon(Icons.add),
+ ),
+ );
+ }),
+ ),
+ ),
+ );
+
+ // Start forwards animation
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+ expect(find.byType(_FlutterLogoModal), findsOneWidget);
+
+ await tester.tapAt(Offset.zero);
+ await tester.pump();
+
+ // Opacity duration: Linear transition throughout 200ms
+ double topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, 1.0);
+
+ // Halfway through forwards animation.
+ await tester.pump(const Duration(milliseconds: 100));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, 0.5);
+
+ // The end of the transition.
+ await tester.pump(const Duration(milliseconds: 100));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, 0.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.byType(_FlutterLogoModal), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'showModal builds a new route with specified barrier properties '
+ 'with default configuration(FadeScaleTransitionConfiguration)',
+ (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Builder(builder: (BuildContext context) {
+ return Center(
+ child: ElevatedButton(
+ onPressed: () {
+ showModal<void>(
+ context: context,
+ builder: (BuildContext context) {
+ return const _FlutterLogoModal();
+ },
+ );
+ },
+ child: const Icon(Icons.add),
+ ),
+ );
+ }),
+ ),
+ ),
+ );
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+
+ // New route containing _FlutterLogoModal is present.
+ expect(find.byType(_FlutterLogoModal), findsOneWidget);
+ final ModalBarrier topModalBarrier = tester.widget<ModalBarrier>(
+ find.byType(ModalBarrier).at(1),
+ );
+
+ // Verify new route's modal barrier properties are correct.
+ expect(topModalBarrier.color, Colors.black54);
+ expect(topModalBarrier.barrierSemanticsDismissible, true);
+ expect(topModalBarrier.semanticsLabel, 'Dismiss');
+ },
+ );
+
+ testWidgets(
+ 'showModal forwards animation '
+ 'with default configuration(FadeScaleTransitionConfiguration)',
+ (WidgetTester tester) async {
+ final GlobalKey key = GlobalKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Builder(builder: (BuildContext context) {
+ return Center(
+ child: ElevatedButton(
+ onPressed: () {
+ showModal<void>(
+ context: context,
+ builder: (BuildContext context) {
+ return _FlutterLogoModal(key: key);
+ },
+ );
+ },
+ child: const Icon(Icons.add),
+ ),
+ );
+ }),
+ ),
+ ),
+ );
+
+ // Start forwards animation
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pump();
+
+ // Opacity duration: First 30% of 150ms, linear transition
+ double topFadeTransitionOpacity = _getOpacity(key, tester);
+ double topScale = _getScale(key, tester);
+ expect(topFadeTransitionOpacity, 0.0);
+ expect(topScale, 0.80);
+
+ // 3/10 * 150ms = 45ms (total opacity animation duration)
+ // 1/2 * 45ms = ~23ms elapsed for halfway point of opacity
+ // animation
+ await tester.pump(const Duration(milliseconds: 23));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, closeTo(0.5, 0.05));
+ topScale = _getScale(key, tester);
+ expect(topScale, greaterThan(0.80));
+ expect(topScale, lessThan(1.0));
+
+ // End of opacity animation.
+ await tester.pump(const Duration(milliseconds: 22));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, 1.0);
+ topScale = _getScale(key, tester);
+ expect(topScale, greaterThan(0.80));
+ expect(topScale, lessThan(1.0));
+
+ // 100ms into the animation
+ await tester.pump(const Duration(milliseconds: 55));
+ topScale = _getScale(key, tester);
+ expect(topScale, greaterThan(0.80));
+ expect(topScale, lessThan(1.0));
+
+ // Get to the end of the animation
+ await tester.pump(const Duration(milliseconds: 50));
+ topScale = _getScale(key, tester);
+ expect(topScale, 1.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.byType(_FlutterLogoModal), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'showModal reverse animation '
+ 'with default configuration(FadeScaleTransitionConfiguration)',
+ (WidgetTester tester) async {
+ final GlobalKey key = GlobalKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Builder(builder: (BuildContext context) {
+ return Center(
+ child: ElevatedButton(
+ onPressed: () {
+ showModal<void>(
+ context: context,
+ builder: (BuildContext context) {
+ return _FlutterLogoModal(key: key);
+ },
+ );
+ },
+ child: const Icon(Icons.add),
+ ),
+ );
+ }),
+ ),
+ ),
+ );
+
+ // Start forwards animation
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+ expect(find.byType(_FlutterLogoModal), findsOneWidget);
+
+ // Tap on modal barrier to start reverse animation.
+ await tester.tapAt(Offset.zero);
+ await tester.pump();
+
+ // Opacity duration: Linear transition throughout 75ms
+ // No scale animations on exit transition.
+ double topFadeTransitionOpacity = _getOpacity(key, tester);
+ double topScale = _getScale(key, tester);
+ expect(topFadeTransitionOpacity, 1.0);
+ expect(topScale, 1.0);
+
+ await tester.pump(const Duration(milliseconds: 25));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ topScale = _getScale(key, tester);
+ expect(topFadeTransitionOpacity, closeTo(0.66, 0.05));
+ expect(topScale, 1.0);
+
+ await tester.pump(const Duration(milliseconds: 25));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ topScale = _getScale(key, tester);
+ expect(topFadeTransitionOpacity, closeTo(0.33, 0.05));
+ expect(topScale, 1.0);
+
+ // End of opacity animation
+ await tester.pump(const Duration(milliseconds: 25));
+ topFadeTransitionOpacity = _getOpacity(key, tester);
+ expect(topFadeTransitionOpacity, 0.0);
+ topScale = _getScale(key, tester);
+ expect(topScale, 1.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.byType(_FlutterLogoModal), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'State is not lost when transitioning',
+ (WidgetTester tester) async {
+ final GlobalKey bottomKey = GlobalKey();
+ final GlobalKey topKey = GlobalKey();
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Builder(builder: (BuildContext context) {
+ return Center(
+ child: Column(
+ children: <Widget>[
+ ElevatedButton(
+ onPressed: () {
+ showModal<void>(
+ context: context,
+ configuration: _TestModalConfiguration(),
+ builder: (BuildContext context) {
+ return _FlutterLogoModal(
+ key: topKey,
+ name: 'top route',
+ );
+ },
+ );
+ },
+ child: const Icon(Icons.add),
+ ),
+ _FlutterLogoModal(
+ key: bottomKey,
+ name: 'bottom route',
+ ),
+ ],
+ ),
+ );
+ }),
+ ),
+ ),
+ );
+
+ // The bottom route's state should already exist.
+ final _FlutterLogoModalState bottomState = tester.state(
+ find.byKey(bottomKey),
+ );
+ expect(bottomState.widget.name, 'bottom route');
+
+ // Start the enter transition of the modal route.
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pump();
+ await tester.pump();
+
+ // The bottom route's state should be retained at the start of the
+ // transition.
+ expect(
+ tester.state(find.byKey(bottomKey)),
+ bottomState,
+ );
+ // The top route's state should be created.
+ final _FlutterLogoModalState topState = tester.state(
+ find.byKey(topKey),
+ );
+ expect(topState.widget.name, 'top route');
+
+ // Halfway point of forwards animation.
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(
+ tester.state(find.byKey(bottomKey)),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(topKey)),
+ topState,
+ );
+
+ // End the transition and see if top and bottom routes'
+ // states persist.
+ await tester.pumpAndSettle();
+ expect(
+ tester.state(find.byKey(
+ bottomKey,
+ skipOffstage: false,
+ )),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(topKey)),
+ topState,
+ );
+
+ // Start the reverse animation. Both top and bottom
+ // routes' states should persist.
+ await tester.tapAt(Offset.zero);
+ await tester.pump();
+ expect(
+ tester.state(find.byKey(bottomKey)),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(topKey)),
+ topState,
+ );
+
+ // Halfway point of the exit transition.
+ await tester.pump(const Duration(milliseconds: 100));
+ expect(
+ tester.state(find.byKey(bottomKey)),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(topKey)),
+ topState,
+ );
+
+ // End the exit transition. The bottom route's state should
+ // persist, whereas the top route's state should no longer
+ // be present.
+ await tester.pumpAndSettle();
+ expect(
+ tester.state(find.byKey(bottomKey)),
+ bottomState,
+ );
+ expect(find.byKey(topKey), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'showModal builds a new route with specified route settings',
+ (WidgetTester tester) async {
+ const RouteSettings routeSettings = RouteSettings(
+ name: 'route-name',
+ arguments: 'arguments',
+ );
+
+ final Widget button = Builder(builder: (BuildContext context) {
+ return Center(
+ child: ElevatedButton(
+ onPressed: () {
+ showModal<void>(
+ context: context,
+ configuration: _TestModalConfiguration(),
+ routeSettings: routeSettings,
+ builder: (BuildContext context) {
+ return const _FlutterLogoModal();
+ },
+ );
+ },
+ child: const Icon(Icons.add),
+ ),
+ );
+ });
+
+ await tester.pumpWidget(_boilerplate(button));
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+
+ // New route containing _FlutterLogoModal is present.
+ expect(find.byType(_FlutterLogoModal), findsOneWidget);
+
+ // Expect the last route pushed to the navigator to contain RouteSettings
+ // equal to the RouteSettings passed to showModal
+ final ModalRoute<dynamic> modalRoute = ModalRoute.of(
+ tester.element(find.byType(_FlutterLogoModal)),
+ )!;
+ expect(modalRoute.settings, routeSettings);
+ },
+ );
+
+ testWidgets(
+ 'showModal builds a new route with specified image filter',
+ (WidgetTester tester) async {
+ final ui.ImageFilter filter = ui.ImageFilter.blur(sigmaX: 1, sigmaY: 1);
+
+ final Widget button = Builder(builder: (BuildContext context) {
+ return Center(
+ child: ElevatedButton(
+ onPressed: () {
+ showModal<void>(
+ context: context,
+ configuration: _TestModalConfiguration(),
+ filter: filter,
+ builder: (BuildContext context) {
+ return const _FlutterLogoModal();
+ },
+ );
+ },
+ child: const Icon(Icons.add),
+ ),
+ );
+ });
+
+ await tester.pumpWidget(_boilerplate(button));
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+
+ // New route containing _FlutterLogoModal is present.
+ expect(find.byType(_FlutterLogoModal), findsOneWidget);
+ final BackdropFilter backdropFilter = tester.widget<BackdropFilter>(
+ find.byType(BackdropFilter),
+ );
+
+ // Verify new route's backdrop filter has been applied
+ expect(backdropFilter.filter, filter);
+ },
+ );
+}
+
+Widget _boilerplate(Widget child) => MaterialApp(home: Scaffold(body: child));
+
+double _getOpacity(GlobalKey key, WidgetTester tester) {
+ final Finder finder = find.ancestor(
+ of: find.byKey(key),
+ matching: find.byType(FadeTransition),
+ );
+ return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
+ final FadeTransition transition = widget as FadeTransition;
+ return a * transition.opacity.value;
+ });
+}
+
+double _getScale(GlobalKey key, WidgetTester tester) {
+ final Finder finder = find.ancestor(
+ of: find.byKey(key),
+ matching: find.byType(ScaleTransition),
+ );
+ return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
+ final ScaleTransition transition = widget as ScaleTransition;
+ return a * transition.scale.value;
+ });
+}
+
+class _FlutterLogoModal extends StatefulWidget {
+ const _FlutterLogoModal({
+ Key? key,
+ this.name,
+ }) : super(key: key);
+
+ final String? name;
+
+ @override
+ _FlutterLogoModalState createState() => _FlutterLogoModalState();
+}
+
+class _FlutterLogoModalState extends State<_FlutterLogoModal> {
+ @override
+ Widget build(BuildContext context) {
+ return const Center(
+ child: SizedBox(
+ width: 250,
+ height: 250,
+ child: Material(
+ child: Center(
+ child: FlutterLogo(size: 250),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _TestModalConfiguration extends ModalConfiguration {
+ _TestModalConfiguration({
+ Color barrierColor = Colors.green,
+ bool barrierDismissible = true,
+ String barrierLabel = 'customLabel',
+ Duration transitionDuration = const Duration(milliseconds: 300),
+ Duration reverseTransitionDuration = const Duration(milliseconds: 200),
+ }) : super(
+ barrierColor: barrierColor,
+ barrierDismissible: barrierDismissible,
+ barrierLabel: barrierLabel,
+ transitionDuration: transitionDuration,
+ reverseTransitionDuration: reverseTransitionDuration,
+ );
+
+ @override
+ Widget transitionBuilder(
+ BuildContext context,
+ Animation<double> animation,
+ Animation<double> secondaryAnimation,
+ Widget child,
+ ) {
+ return FadeTransition(
+ opacity: animation,
+ child: child,
+ );
+ }
+}
diff --git a/packages/animations/test/open_container_test.dart b/packages/animations/test/open_container_test.dart
new file mode 100644
index 0000000..1b05f44
--- /dev/null
+++ b/packages/animations/test/open_container_test.dart
@@ -0,0 +1,1978 @@
+// 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:animations/src/open_container.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ testWidgets(
+ 'Container opens - Fade (by default)',
+ (WidgetTester tester) async {
+ const ShapeBorder shape = RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(8.0)),
+ );
+ bool closedBuilderCalled = false;
+ bool openBuilderCalled = false;
+
+ await tester.pumpWidget(_boilerplate(
+ child: Center(
+ child: OpenContainer(
+ closedColor: Colors.green,
+ openColor: Colors.blue,
+ closedElevation: 4.0,
+ openElevation: 8.0,
+ closedShape: shape,
+ closedBuilder: (BuildContext context, VoidCallback _) {
+ closedBuilderCalled = true;
+ return const Text('Closed');
+ },
+ openBuilder: (BuildContext context, VoidCallback _) {
+ openBuilderCalled = true;
+ return const Text('Open');
+ },
+ ),
+ ),
+ ));
+
+ // Closed container has the expected properties.
+ final StatefulElement srcMaterialElement = tester.firstElement(
+ find.ancestor(
+ of: find.text('Closed'),
+ matching: find.byType(Material),
+ ),
+ );
+ final Material srcMaterial = srcMaterialElement.widget as Material;
+ expect(srcMaterial.color, Colors.green);
+ expect(srcMaterial.elevation, 4.0);
+ expect(srcMaterial.shape, shape);
+ expect(find.text('Closed'), findsOneWidget);
+ expect(find.text('Open'), findsNothing);
+ expect(closedBuilderCalled, isTrue);
+ expect(openBuilderCalled, isFalse);
+ final Rect srcMaterialRect = tester.getRect(
+ find.byElementPredicate((Element e) => e == srcMaterialElement),
+ );
+
+ // Open the container.
+ await tester.tap(find.text('Closed'));
+ expect(find.text('Closed'), findsOneWidget);
+ expect(find.text('Open'), findsNothing);
+ await tester.pump();
+
+ // On the first frame of the animation everything still looks like before.
+ final StatefulElement destMaterialElement = tester.firstElement(
+ find.ancestor(
+ of: find.text('Closed'),
+ matching: find.byType(Material),
+ ),
+ );
+ final Material closedMaterial = destMaterialElement.widget as Material;
+ expect(closedMaterial.color, Colors.green);
+ expect(closedMaterial.elevation, 4.0);
+ expect(closedMaterial.shape, shape);
+ expect(find.text('Closed'), findsOneWidget);
+ expect(find.text('Open'), findsOneWidget);
+ final Rect closedMaterialRect = tester.getRect(
+ find.byElementPredicate((Element e) => e == destMaterialElement),
+ );
+ expect(closedMaterialRect, srcMaterialRect);
+ expect(_getOpacity(tester, 'Open'), 0.0);
+ expect(_getOpacity(tester, 'Closed'), 1.0);
+
+ final _TrackedData dataClosed = _TrackedData(
+ closedMaterial,
+ closedMaterialRect,
+ );
+
+ // Jump to the start of the fade in.
+ await tester.pump(const Duration(milliseconds: 60)); // 300ms * 1/5 = 60ms
+ final _TrackedData dataPreFade = _TrackedData(
+ destMaterialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == destMaterialElement),
+ ),
+ );
+ _expectMaterialPropertiesHaveAdvanced(
+ smallerMaterial: dataClosed,
+ biggerMaterial: dataPreFade,
+ tester: tester,
+ );
+ expect(_getOpacity(tester, 'Open'), moreOrLessEquals(0.0));
+ expect(_getOpacity(tester, 'Closed'), 1.0);
+
+ // Jump to the middle of the fade in.
+ await tester
+ .pump(const Duration(milliseconds: 30)); // 300ms * 3/10 = 90ms
+ final _TrackedData dataMidFadeIn = _TrackedData(
+ destMaterialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == destMaterialElement),
+ ),
+ );
+ _expectMaterialPropertiesHaveAdvanced(
+ smallerMaterial: dataPreFade,
+ biggerMaterial: dataMidFadeIn,
+ tester: tester,
+ );
+ expect(dataMidFadeIn.material.color, isNot(dataPreFade.material.color));
+ expect(_getOpacity(tester, 'Open'), lessThan(1.0));
+ expect(_getOpacity(tester, 'Open'), greaterThan(0.0));
+ expect(_getOpacity(tester, 'Closed'), 1.0);
+
+ // Jump to the end of the fade in at 2/5 of 300ms.
+ await tester.pump(
+ const Duration(milliseconds: 30),
+ ); // 300ms * 2/5 = 120ms
+
+ final _TrackedData dataPostFadeIn = _TrackedData(
+ destMaterialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == destMaterialElement),
+ ),
+ );
+ _expectMaterialPropertiesHaveAdvanced(
+ smallerMaterial: dataMidFadeIn,
+ biggerMaterial: dataPostFadeIn,
+ tester: tester,
+ );
+ expect(_getOpacity(tester, 'Open'), moreOrLessEquals(1.0));
+ expect(_getOpacity(tester, 'Closed'), 1.0);
+
+ // Jump almost to the end of the transition.
+ await tester.pump(const Duration(milliseconds: 180));
+ final _TrackedData dataTransitionDone = _TrackedData(
+ destMaterialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == destMaterialElement),
+ ),
+ );
+ _expectMaterialPropertiesHaveAdvanced(
+ smallerMaterial: dataMidFadeIn,
+ biggerMaterial: dataTransitionDone,
+ tester: tester,
+ );
+ expect(_getOpacity(tester, 'Open'), 1.0);
+ expect(dataTransitionDone.material.color, Colors.blue);
+ expect(dataTransitionDone.material.elevation, 8.0);
+ expect(dataTransitionDone.radius, 0.0);
+ expect(dataTransitionDone.rect, const Rect.fromLTRB(0, 0, 800, 600));
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text('Closed'), findsNothing); // No longer in the tree.
+ expect(find.text('Open'), findsOneWidget);
+ final StatefulElement finalMaterialElement = tester.firstElement(
+ find.ancestor(
+ of: find.text('Open'),
+ matching: find.byType(Material),
+ ),
+ );
+ final _TrackedData dataOpen = _TrackedData(
+ finalMaterialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == finalMaterialElement),
+ ),
+ );
+ expect(dataOpen.material.color, dataTransitionDone.material.color);
+ expect(
+ dataOpen.material.elevation, dataTransitionDone.material.elevation);
+ expect(dataOpen.radius, dataTransitionDone.radius);
+ expect(dataOpen.rect, dataTransitionDone.rect);
+ },
+ );
+
+ testWidgets(
+ 'Container closes - Fade (by default)',
+ (WidgetTester tester) async {
+ const ShapeBorder shape = RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(8.0)),
+ );
+
+ await tester.pumpWidget(_boilerplate(
+ child: Center(
+ child: OpenContainer(
+ closedColor: Colors.green,
+ openColor: Colors.blue,
+ closedElevation: 4.0,
+ openElevation: 8.0,
+ closedShape: shape,
+ closedBuilder: (BuildContext context, VoidCallback _) {
+ return const Text('Closed');
+ },
+ openBuilder: (BuildContext context, VoidCallback _) {
+ return const Text('Open');
+ },
+ ),
+ ),
+ ));
+
+ await tester.tap(find.text('Closed'));
+ await tester.pumpAndSettle();
+
+ // Open container has the expected properties.
+ expect(find.text('Closed'), findsNothing);
+ expect(find.text('Open'), findsOneWidget);
+ final StatefulElement initialMaterialElement = tester.firstElement(
+ find.ancestor(
+ of: find.text('Open'),
+ matching: find.byType(Material),
+ ),
+ );
+ final _TrackedData dataOpen = _TrackedData(
+ initialMaterialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == initialMaterialElement),
+ ),
+ );
+ expect(dataOpen.material.color, Colors.blue);
+ expect(dataOpen.material.elevation, 8.0);
+ expect(dataOpen.radius, 0.0);
+ expect(dataOpen.rect, const Rect.fromLTRB(0, 0, 800, 600));
+
+ // Close the container.
+ final NavigatorState navigator = tester.state(find.byType(Navigator));
+ navigator.pop();
+ await tester.pump();
+
+ expect(find.text('Closed'), findsOneWidget);
+ expect(find.text('Open'), findsOneWidget);
+ final StatefulElement materialElement = tester.firstElement(
+ find.ancestor(
+ of: find.text('Open'),
+ matching: find.byType(Material),
+ ),
+ );
+ final _TrackedData dataTransitionStart = _TrackedData(
+ materialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == materialElement),
+ ),
+ );
+ expect(dataTransitionStart.material.color, dataOpen.material.color);
+ expect(
+ dataTransitionStart.material.elevation, dataOpen.material.elevation);
+ expect(dataTransitionStart.radius, dataOpen.radius);
+ expect(dataTransitionStart.rect, dataOpen.rect);
+ expect(_getOpacity(tester, 'Open'), 1.0);
+ await tester.pump(const Duration(microseconds: 1)); // 300 * 1/5 = 60
+
+ // Jump to start of fade out: 1/5 of 300.
+ await tester.pump(const Duration(milliseconds: 60)); // 300 * 1/5 = 60
+ final _TrackedData dataPreFadeOut = _TrackedData(
+ materialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == materialElement),
+ ),
+ );
+ _expectMaterialPropertiesHaveAdvanced(
+ smallerMaterial: dataPreFadeOut,
+ biggerMaterial: dataTransitionStart,
+ tester: tester,
+ );
+ expect(_getOpacity(tester, 'Open'), moreOrLessEquals(1.0));
+ expect(_getOpacity(tester, 'Closed'), 1.0);
+
+ // Jump to the middle of the fade out.
+ await tester.pump(const Duration(milliseconds: 30)); // 300 * 3/10 = 90
+ final _TrackedData dataMidpoint = _TrackedData(
+ materialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == materialElement),
+ ),
+ );
+ _expectMaterialPropertiesHaveAdvanced(
+ smallerMaterial: dataMidpoint,
+ biggerMaterial: dataPreFadeOut,
+ tester: tester,
+ );
+ expect(dataMidpoint.material.color, isNot(dataPreFadeOut.material.color));
+ expect(_getOpacity(tester, 'Open'), lessThan(1.0));
+ expect(_getOpacity(tester, 'Open'), greaterThan(0.0));
+ expect(_getOpacity(tester, 'Closed'), 1.0);
+
+ // Jump to the end of the fade out.
+ await tester.pump(const Duration(milliseconds: 30)); // 300 * 2/5 = 120
+ final _TrackedData dataPostFadeOut = _TrackedData(
+ materialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == materialElement),
+ ),
+ );
+ _expectMaterialPropertiesHaveAdvanced(
+ smallerMaterial: dataPostFadeOut,
+ biggerMaterial: dataMidpoint,
+ tester: tester,
+ );
+ expect(_getOpacity(tester, 'Open'), moreOrLessEquals(0.0));
+ expect(_getOpacity(tester, 'Closed'), 1.0);
+
+ // Jump almost to the end of the transition.
+ await tester.pump(const Duration(milliseconds: 180));
+ final _TrackedData dataTransitionDone = _TrackedData(
+ materialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == materialElement),
+ ),
+ );
+ _expectMaterialPropertiesHaveAdvanced(
+ smallerMaterial: dataTransitionDone,
+ biggerMaterial: dataPostFadeOut,
+ tester: tester,
+ );
+ expect(_getOpacity(tester, 'Closed'), 1.0);
+ expect(_getOpacity(tester, 'Open'), 0.0);
+ expect(dataTransitionDone.material.color, Colors.green);
+ expect(dataTransitionDone.material.elevation, 4.0);
+ expect(dataTransitionDone.radius, 8.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text('Open'), findsNothing); // No longer in the tree.
+ expect(find.text('Closed'), findsOneWidget);
+ final StatefulElement finalMaterialElement = tester.firstElement(
+ find.ancestor(
+ of: find.text('Closed'),
+ matching: find.byType(Material),
+ ),
+ );
+ final _TrackedData dataClosed = _TrackedData(
+ finalMaterialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == finalMaterialElement),
+ ),
+ );
+ expect(dataClosed.material.color, dataTransitionDone.material.color);
+ expect(
+ dataClosed.material.elevation,
+ dataTransitionDone.material.elevation,
+ );
+ expect(dataClosed.radius, dataTransitionDone.radius);
+ expect(dataClosed.rect, dataTransitionDone.rect);
+ },
+ );
+
+ testWidgets('Container opens - Fade through', (WidgetTester tester) async {
+ const ShapeBorder shape = RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(8.0)),
+ );
+ bool closedBuilderCalled = false;
+ bool openBuilderCalled = false;
+
+ await tester.pumpWidget(_boilerplate(
+ child: Center(
+ child: OpenContainer(
+ closedColor: Colors.green,
+ openColor: Colors.blue,
+ middleColor: Colors.red,
+ closedElevation: 4.0,
+ openElevation: 8.0,
+ closedShape: shape,
+ closedBuilder: (BuildContext context, VoidCallback _) {
+ closedBuilderCalled = true;
+ return const Text('Closed');
+ },
+ openBuilder: (BuildContext context, VoidCallback _) {
+ openBuilderCalled = true;
+ return const Text('Open');
+ },
+ transitionType: ContainerTransitionType.fadeThrough,
+ ),
+ ),
+ ));
+
+ // Closed container has the expected properties.
+ final StatefulElement srcMaterialElement = tester.firstElement(
+ find.ancestor(
+ of: find.text('Closed'),
+ matching: find.byType(Material),
+ ),
+ );
+ final Material srcMaterial = srcMaterialElement.widget as Material;
+ expect(srcMaterial.color, Colors.green);
+ expect(srcMaterial.elevation, 4.0);
+ expect(srcMaterial.shape, shape);
+ expect(find.text('Closed'), findsOneWidget);
+ expect(find.text('Open'), findsNothing);
+ expect(closedBuilderCalled, isTrue);
+ expect(openBuilderCalled, isFalse);
+ final Rect srcMaterialRect = tester.getRect(
+ find.byElementPredicate((Element e) => e == srcMaterialElement),
+ );
+
+ // Open the container.
+ await tester.tap(find.text('Closed'));
+ expect(find.text('Closed'), findsOneWidget);
+ expect(find.text('Open'), findsNothing);
+ await tester.pump();
+
+ // On the first frame of the animation everything still looks like before.
+ final StatefulElement destMaterialElement = tester.firstElement(
+ find.ancestor(
+ of: find.text('Closed'),
+ matching: find.byType(Material),
+ ),
+ );
+ final Material closedMaterial = destMaterialElement.widget as Material;
+ expect(closedMaterial.color, Colors.green);
+ expect(closedMaterial.elevation, 4.0);
+ expect(closedMaterial.shape, shape);
+ expect(find.text('Closed'), findsOneWidget);
+ expect(find.text('Open'), findsOneWidget);
+ final Rect closedMaterialRect = tester.getRect(
+ find.byElementPredicate((Element e) => e == destMaterialElement),
+ );
+ expect(closedMaterialRect, srcMaterialRect);
+ expect(_getOpacity(tester, 'Open'), 0.0);
+ expect(_getOpacity(tester, 'Closed'), 1.0);
+
+ final _TrackedData dataClosed = _TrackedData(
+ closedMaterial,
+ closedMaterialRect,
+ );
+
+ // The fade-out takes 1/5 of 300ms. Let's jump to the midpoint of that.
+ await tester.pump(const Duration(milliseconds: 30)); // 300ms * 1/10 = 30ms
+ final _TrackedData dataMidFadeOut = _TrackedData(
+ destMaterialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == destMaterialElement),
+ ),
+ );
+ _expectMaterialPropertiesHaveAdvanced(
+ smallerMaterial: dataClosed,
+ biggerMaterial: dataMidFadeOut,
+ tester: tester,
+ );
+ expect(dataMidFadeOut.material.color, isNot(dataClosed.material.color));
+ expect(_getOpacity(tester, 'Open'), 0.0);
+ expect(_getOpacity(tester, 'Closed'), lessThan(1.0));
+ expect(_getOpacity(tester, 'Closed'), greaterThan(0.0));
+
+ // Let's jump to the crossover point at 1/5 of 300ms.
+ await tester.pump(const Duration(milliseconds: 30)); // 300ms * 1/5 = 60ms
+ final _TrackedData dataMidpoint = _TrackedData(
+ destMaterialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == destMaterialElement),
+ ),
+ );
+ _expectMaterialPropertiesHaveAdvanced(
+ smallerMaterial: dataMidFadeOut,
+ biggerMaterial: dataMidpoint,
+ tester: tester,
+ );
+ expect(dataMidpoint.material.color, Colors.red);
+ expect(_getOpacity(tester, 'Open'), moreOrLessEquals(0.0));
+ expect(_getOpacity(tester, 'Closed'), moreOrLessEquals(0.0));
+
+ // Let's jump to the middle of the fade-in at 3/5 of 300ms
+ await tester.pump(const Duration(milliseconds: 120)); // 300ms * 3/5 = 180ms
+ final _TrackedData dataMidFadeIn = _TrackedData(
+ destMaterialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == destMaterialElement),
+ ),
+ );
+ _expectMaterialPropertiesHaveAdvanced(
+ smallerMaterial: dataMidpoint,
+ biggerMaterial: dataMidFadeIn,
+ tester: tester,
+ );
+ expect(dataMidFadeIn.material.color, isNot(dataMidpoint.material.color));
+ expect(_getOpacity(tester, 'Open'), lessThan(1.0));
+ expect(_getOpacity(tester, 'Open'), greaterThan(0.0));
+ expect(_getOpacity(tester, 'Closed'), 0.0);
+
+ // Let's jump almost to the end of the transition.
+ await tester.pump(const Duration(milliseconds: 120));
+ final _TrackedData dataTransitionDone = _TrackedData(
+ destMaterialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == destMaterialElement),
+ ),
+ );
+ _expectMaterialPropertiesHaveAdvanced(
+ smallerMaterial: dataMidFadeIn,
+ biggerMaterial: dataTransitionDone,
+ tester: tester,
+ );
+ expect(
+ dataTransitionDone.material.color,
+ isNot(dataMidFadeIn.material.color),
+ );
+ expect(_getOpacity(tester, 'Open'), 1.0);
+ expect(_getOpacity(tester, 'Closed'), 0.0);
+ expect(dataTransitionDone.material.color, Colors.blue);
+ expect(dataTransitionDone.material.elevation, 8.0);
+ expect(dataTransitionDone.radius, 0.0);
+ expect(dataTransitionDone.rect, const Rect.fromLTRB(0, 0, 800, 600));
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text('Closed'), findsNothing); // No longer in the tree.
+ expect(find.text('Open'), findsOneWidget);
+ final StatefulElement finalMaterialElement = tester.firstElement(
+ find.ancestor(
+ of: find.text('Open'),
+ matching: find.byType(Material),
+ ),
+ );
+ final _TrackedData dataOpen = _TrackedData(
+ finalMaterialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == finalMaterialElement),
+ ),
+ );
+ expect(dataOpen.material.color, dataTransitionDone.material.color);
+ expect(dataOpen.material.elevation, dataTransitionDone.material.elevation);
+ expect(dataOpen.radius, dataTransitionDone.radius);
+ expect(dataOpen.rect, dataTransitionDone.rect);
+ });
+
+ testWidgets('Container closes - Fade through', (WidgetTester tester) async {
+ const ShapeBorder shape = RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(8.0)),
+ );
+
+ await tester.pumpWidget(_boilerplate(
+ child: Center(
+ child: OpenContainer(
+ closedColor: Colors.green,
+ openColor: Colors.blue,
+ middleColor: Colors.red,
+ closedElevation: 4.0,
+ openElevation: 8.0,
+ closedShape: shape,
+ closedBuilder: (BuildContext context, VoidCallback _) {
+ return const Text('Closed');
+ },
+ openBuilder: (BuildContext context, VoidCallback _) {
+ return const Text('Open');
+ },
+ transitionType: ContainerTransitionType.fadeThrough,
+ ),
+ ),
+ ));
+
+ await tester.tap(find.text('Closed'));
+ await tester.pumpAndSettle();
+
+ // Open container has the expected properties.
+ expect(find.text('Closed'), findsNothing);
+ expect(find.text('Open'), findsOneWidget);
+ final StatefulElement initialMaterialElement = tester.firstElement(
+ find.ancestor(
+ of: find.text('Open'),
+ matching: find.byType(Material),
+ ),
+ );
+ final _TrackedData dataOpen = _TrackedData(
+ initialMaterialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == initialMaterialElement),
+ ),
+ );
+ expect(dataOpen.material.color, Colors.blue);
+ expect(dataOpen.material.elevation, 8.0);
+ expect(dataOpen.radius, 0.0);
+ expect(dataOpen.rect, const Rect.fromLTRB(0, 0, 800, 600));
+
+ // Close the container.
+ final NavigatorState navigator = tester.state(find.byType(Navigator));
+ navigator.pop();
+ await tester.pump();
+
+ expect(find.text('Closed'), findsOneWidget);
+ expect(find.text('Open'), findsOneWidget);
+ final StatefulElement materialElement = tester.firstElement(
+ find.ancestor(
+ of: find.text('Open'),
+ matching: find.byType(Material),
+ ),
+ );
+ final _TrackedData dataTransitionStart = _TrackedData(
+ materialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == materialElement),
+ ),
+ );
+ expect(dataTransitionStart.material.color, dataOpen.material.color);
+ expect(dataTransitionStart.material.elevation, dataOpen.material.elevation);
+ expect(dataTransitionStart.radius, dataOpen.radius);
+ expect(dataTransitionStart.rect, dataOpen.rect);
+ expect(_getOpacity(tester, 'Open'), 1.0);
+ expect(_getOpacity(tester, 'Closed'), 0.0);
+
+ // Jump to mid-point of fade-out: 1/10 of 300ms.
+ await tester.pump(const Duration(milliseconds: 30)); // 300ms * 1/10 = 30ms
+ final _TrackedData dataMidFadeOut = _TrackedData(
+ materialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == materialElement),
+ ),
+ );
+ _expectMaterialPropertiesHaveAdvanced(
+ smallerMaterial: dataMidFadeOut,
+ biggerMaterial: dataTransitionStart,
+ tester: tester,
+ );
+ expect(
+ dataMidFadeOut.material.color,
+ isNot(dataTransitionStart.material.color),
+ );
+ expect(_getOpacity(tester, 'Closed'), 0.0);
+ expect(_getOpacity(tester, 'Open'), lessThan(1.0));
+ expect(_getOpacity(tester, 'Open'), greaterThan(0.0));
+
+ // Let's jump to the crossover point at 1/5 of 300ms.
+ await tester.pump(const Duration(milliseconds: 30)); // 300ms * 1/5 = 60ms
+ final _TrackedData dataMidpoint = _TrackedData(
+ materialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == materialElement),
+ ),
+ );
+ _expectMaterialPropertiesHaveAdvanced(
+ smallerMaterial: dataMidpoint,
+ biggerMaterial: dataMidFadeOut,
+ tester: tester,
+ );
+ expect(dataMidpoint.material.color, Colors.red);
+ expect(_getOpacity(tester, 'Open'), moreOrLessEquals(0.0));
+ expect(_getOpacity(tester, 'Closed'), moreOrLessEquals(0.0));
+
+ // Let's jump to the middle of the fade-in at 3/5 of 300ms
+ await tester.pump(const Duration(milliseconds: 120)); // 300ms * 3/5 = 180ms
+ final _TrackedData dataMidFadeIn = _TrackedData(
+ materialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == materialElement),
+ ),
+ );
+ _expectMaterialPropertiesHaveAdvanced(
+ smallerMaterial: dataMidFadeIn,
+ biggerMaterial: dataMidpoint,
+ tester: tester,
+ );
+ expect(dataMidFadeIn.material.color, isNot(dataMidpoint.material.color));
+ expect(_getOpacity(tester, 'Closed'), lessThan(1.0));
+ expect(_getOpacity(tester, 'Closed'), greaterThan(0.0));
+ expect(_getOpacity(tester, 'Open'), 0.0);
+
+ // Let's jump almost to the end of the transition.
+ await tester.pump(const Duration(milliseconds: 120));
+ final _TrackedData dataTransitionDone = _TrackedData(
+ materialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == materialElement),
+ ),
+ );
+ _expectMaterialPropertiesHaveAdvanced(
+ smallerMaterial: dataTransitionDone,
+ biggerMaterial: dataMidFadeIn,
+ tester: tester,
+ );
+ expect(
+ dataTransitionDone.material.color,
+ isNot(dataMidFadeIn.material.color),
+ );
+ expect(_getOpacity(tester, 'Closed'), 1.0);
+ expect(_getOpacity(tester, 'Open'), 0.0);
+ expect(dataTransitionDone.material.color, Colors.green);
+ expect(dataTransitionDone.material.elevation, 4.0);
+ expect(dataTransitionDone.radius, 8.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text('Open'), findsNothing); // No longer in the tree.
+ expect(find.text('Closed'), findsOneWidget);
+ final StatefulElement finalMaterialElement = tester.firstElement(
+ find.ancestor(
+ of: find.text('Closed'),
+ matching: find.byType(Material),
+ ),
+ );
+ final _TrackedData dataClosed = _TrackedData(
+ finalMaterialElement.widget as Material,
+ tester.getRect(
+ find.byElementPredicate((Element e) => e == finalMaterialElement),
+ ),
+ );
+ expect(dataClosed.material.color, dataTransitionDone.material.color);
+ expect(
+ dataClosed.material.elevation,
+ dataTransitionDone.material.elevation,
+ );
+ expect(dataClosed.radius, dataTransitionDone.radius);
+ expect(dataClosed.rect, dataTransitionDone.rect);
+ });
+
+ testWidgets('Cannot tap container if tappable=false',
+ (WidgetTester tester) async {
+ await tester.pumpWidget(_boilerplate(
+ child: Center(
+ child: OpenContainer(
+ tappable: false,
+ closedBuilder: (BuildContext context, VoidCallback _) {
+ return const Text('Closed');
+ },
+ openBuilder: (BuildContext context, VoidCallback _) {
+ return const Text('Open');
+ },
+ ),
+ ),
+ ));
+
+ expect(find.text('Open'), findsNothing);
+ expect(find.text('Closed'), findsOneWidget);
+ await tester.tap(find.text('Closed'));
+ await tester.pumpAndSettle();
+ expect(find.text('Open'), findsNothing);
+ expect(find.text('Closed'), findsOneWidget);
+ });
+
+ testWidgets('Action callbacks work', (WidgetTester tester) async {
+ late VoidCallback open, close;
+ await tester.pumpWidget(_boilerplate(
+ child: Center(
+ child: OpenContainer(
+ tappable: false,
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ open = action;
+ return const Text('Closed');
+ },
+ openBuilder: (BuildContext context, VoidCallback action) {
+ close = action;
+ return const Text('Open');
+ },
+ ),
+ ),
+ ));
+
+ expect(find.text('Open'), findsNothing);
+ expect(find.text('Closed'), findsOneWidget);
+
+ expect(open, isNotNull);
+ open();
+ await tester.pumpAndSettle();
+
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsNothing);
+
+ expect(close, isNotNull);
+ close();
+ await tester.pumpAndSettle();
+
+ expect(find.text('Open'), findsNothing);
+ expect(find.text('Closed'), findsOneWidget);
+ });
+
+ testWidgets('open widget keeps state', (WidgetTester tester) async {
+ await tester.pumpWidget(_boilerplate(
+ child: Center(
+ child: OpenContainer(
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ return const Text('Closed');
+ },
+ openBuilder: (BuildContext context, VoidCallback action) {
+ return const DummyStatefulWidget();
+ },
+ ),
+ ),
+ ));
+
+ await tester.tap(find.text('Closed'));
+ await tester.pump(const Duration(milliseconds: 200));
+
+ final State stateOpening = tester.state(find.byType(DummyStatefulWidget));
+ expect(stateOpening, isNotNull);
+
+ await tester.pumpAndSettle();
+ expect(find.text('Closed'), findsNothing);
+ final State stateOpen = tester.state(find.byType(DummyStatefulWidget));
+ expect(stateOpen, isNotNull);
+ expect(stateOpen, same(stateOpening));
+
+ final NavigatorState navigator = tester.state(find.byType(Navigator));
+ navigator.pop();
+ await tester.pump(const Duration(milliseconds: 200));
+ expect(find.text('Closed'), findsOneWidget);
+ final State stateClosing = tester.state(find.byType(DummyStatefulWidget));
+ expect(stateClosing, isNotNull);
+ expect(stateClosing, same(stateOpen));
+ });
+
+ testWidgets('closed widget keeps state', (WidgetTester tester) async {
+ late VoidCallback open;
+ await tester.pumpWidget(_boilerplate(
+ child: Center(
+ child: OpenContainer(
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ open = action;
+ return const DummyStatefulWidget();
+ },
+ openBuilder: (BuildContext context, VoidCallback action) {
+ return const Text('Open');
+ },
+ ),
+ ),
+ ));
+
+ final State stateClosed = tester.state(find.byType(DummyStatefulWidget));
+ expect(stateClosed, isNotNull);
+
+ open();
+ await tester.pump(const Duration(milliseconds: 200));
+ expect(find.text('Open'), findsOneWidget);
+
+ final State stateOpening = tester.state(find.byType(DummyStatefulWidget));
+ expect(stateOpening, same(stateClosed));
+
+ await tester.pumpAndSettle();
+ expect(find.byType(DummyStatefulWidget), findsNothing);
+ expect(find.text('Open'), findsOneWidget);
+ final State stateOpen = tester.state(find.byType(
+ DummyStatefulWidget,
+ skipOffstage: false,
+ ));
+ expect(stateOpen, same(stateOpening));
+
+ final NavigatorState navigator = tester.state(find.byType(Navigator));
+ navigator.pop();
+ await tester.pump(const Duration(milliseconds: 200));
+ expect(find.text('Open'), findsOneWidget);
+ final State stateClosing = tester.state(find.byType(DummyStatefulWidget));
+ expect(stateClosing, same(stateOpen));
+
+ await tester.pumpAndSettle();
+ expect(find.text('Open'), findsNothing);
+ final State stateClosedAgain =
+ tester.state(find.byType(DummyStatefulWidget));
+ expect(stateClosedAgain, same(stateClosing));
+ });
+
+ testWidgets('closes to the right location when src position has changed',
+ (WidgetTester tester) async {
+ final Widget openContainer = OpenContainer(
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ return const SizedBox(
+ height: 100,
+ width: 100,
+ child: Text('Closed'),
+ );
+ },
+ openBuilder: (BuildContext context, VoidCallback action) {
+ return GestureDetector(
+ onTap: action,
+ child: const Text('Open'),
+ );
+ },
+ );
+
+ await tester.pumpWidget(_boilerplate(
+ child: Align(
+ alignment: Alignment.topLeft,
+ child: openContainer,
+ ),
+ ));
+
+ final Rect originTextRect = tester.getRect(find.text('Closed'));
+ expect(originTextRect.topLeft, Offset.zero);
+
+ await tester.tap(find.text('Closed'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsNothing);
+
+ await tester.pumpWidget(_boilerplate(
+ child: Align(
+ alignment: Alignment.bottomLeft,
+ child: openContainer,
+ ),
+ ));
+
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsNothing);
+
+ await tester.tap(find.text('Open'));
+ await tester.pump(); // Need one frame to measure things in the old route.
+ await tester.pump(const Duration(milliseconds: 300));
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsOneWidget);
+
+ final Rect transitionEndTextRect = tester.getRect(find.text('Open'));
+ expect(transitionEndTextRect.topLeft, const Offset(0.0, 600.0 - 100.0));
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text('Open'), findsNothing);
+ expect(find.text('Closed'), findsOneWidget);
+
+ final Rect finalTextRect = tester.getRect(find.text('Closed'));
+ expect(finalTextRect.topLeft, transitionEndTextRect.topLeft);
+ });
+
+ testWidgets('src changes size while open', (WidgetTester tester) async {
+ final Widget openContainer = _boilerplate(
+ child: Center(
+ child: OpenContainer(
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ return const _SizableContainer(
+ initialSize: 100,
+ child: Text('Closed'),
+ );
+ },
+ openBuilder: (BuildContext context, VoidCallback action) {
+ return GestureDetector(
+ onTap: action,
+ child: const Text('Open'),
+ );
+ },
+ ),
+ ),
+ );
+
+ await tester.pumpWidget(openContainer);
+
+ final Size orignalClosedRect = tester.getSize(find
+ .ancestor(
+ of: find.text('Closed'),
+ matching: find.byType(Material),
+ )
+ .first);
+ expect(orignalClosedRect, const Size(100, 100));
+
+ await tester.tap(find.text('Closed'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsNothing);
+
+ final _SizableContainerState containerState = tester.state(find.byType(
+ _SizableContainer,
+ skipOffstage: false,
+ ));
+ containerState.size = 200;
+
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsNothing);
+ await tester.tap(find.text('Open'));
+ await tester.pump(); // Need one frame to measure things in the old route.
+ await tester.pump(const Duration(milliseconds: 300));
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsOneWidget);
+
+ final Size transitionEndSize = tester.getSize(find
+ .ancestor(
+ of: find.text('Open'),
+ matching: find.byType(Material),
+ )
+ .first);
+ expect(transitionEndSize, const Size(200, 200));
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text('Open'), findsNothing);
+ expect(find.text('Closed'), findsOneWidget);
+
+ final Size finalSize = tester.getSize(find
+ .ancestor(
+ of: find.text('Closed'),
+ matching: find.byType(Material),
+ )
+ .first);
+ expect(finalSize, const Size(200, 200));
+ });
+
+ testWidgets('transition is interrupted and should not jump',
+ (WidgetTester tester) async {
+ await tester.pumpWidget(_boilerplate(
+ child: Center(
+ child: OpenContainer(
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ return const Text('Closed');
+ },
+ openBuilder: (BuildContext context, VoidCallback action) {
+ return const Text('Open');
+ },
+ ),
+ ),
+ ));
+
+ await tester.tap(find.text('Closed'));
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsOneWidget);
+
+ final Material openingMaterial = tester.firstWidget(find.ancestor(
+ of: find.text('Closed'),
+ matching: find.byType(Material),
+ ));
+ final Rect openingRect = tester.getRect(
+ find.byWidgetPredicate((Widget w) => w == openingMaterial),
+ );
+
+ // Close the container while it is half way to open.
+ final NavigatorState navigator = tester.state(find.byType(Navigator));
+ navigator.pop();
+ await tester.pump();
+
+ final Material closingMaterial = tester.firstWidget(find.ancestor(
+ of: find.text('Open'),
+ matching: find.byType(Material),
+ ));
+ final Rect closingRect = tester.getRect(
+ find.byWidgetPredicate((Widget w) => w == closingMaterial),
+ );
+
+ expect(closingMaterial.elevation, openingMaterial.elevation);
+ expect(closingMaterial.color, openingMaterial.color);
+ expect(closingMaterial.shape, openingMaterial.shape);
+ expect(closingRect, openingRect);
+ });
+
+ testWidgets('navigator is not full size', (WidgetTester tester) async {
+ await tester.pumpWidget(Center(
+ child: SizedBox(
+ width: 300,
+ height: 400,
+ child: _boilerplate(
+ child: Center(
+ child: OpenContainer(
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ return const Text('Closed');
+ },
+ openBuilder: (BuildContext context, VoidCallback action) {
+ return const Text('Open');
+ },
+ ),
+ ),
+ ),
+ ),
+ ));
+ const Rect fullNavigator = Rect.fromLTWH(250, 100, 300, 400);
+
+ expect(tester.getRect(find.byType(Navigator)), fullNavigator);
+ final Rect materialRectClosed = tester.getRect(find
+ .ancestor(
+ of: find.text('Closed'),
+ matching: find.byType(Material),
+ )
+ .first);
+
+ await tester.tap(find.text('Closed'));
+ await tester.pump();
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsOneWidget);
+ final Rect materialRectTransitionStart = tester.getRect(find.ancestor(
+ of: find.text('Closed'),
+ matching: find.byType(Material),
+ ));
+ expect(materialRectTransitionStart, materialRectClosed);
+
+ await tester.pump(const Duration(milliseconds: 300));
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsOneWidget);
+ final Rect materialRectTransitionEnd = tester.getRect(find.ancestor(
+ of: find.text('Open'),
+ matching: find.byType(Material),
+ ));
+ expect(materialRectTransitionEnd, fullNavigator);
+ await tester.pumpAndSettle();
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsNothing);
+ final Rect materialRectOpen = tester.getRect(find.ancestor(
+ of: find.text('Open'),
+ matching: find.byType(Material),
+ ));
+ expect(materialRectOpen, fullNavigator);
+ });
+
+ testWidgets('does not crash when disposed right after pop',
+ (WidgetTester tester) async {
+ await tester.pumpWidget(Center(
+ child: SizedBox(
+ width: 300,
+ height: 400,
+ child: _boilerplate(
+ child: Center(
+ child: OpenContainer(
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ return const Text('Closed');
+ },
+ openBuilder: (BuildContext context, VoidCallback action) {
+ return const Text('Open');
+ },
+ ),
+ ),
+ ),
+ ),
+ ));
+
+ await tester.tap(find.text('Closed'));
+ await tester.pumpAndSettle();
+
+ final NavigatorState navigator = tester.state(find.byType(Navigator));
+ navigator.pop();
+
+ await tester.pumpWidget(const Placeholder());
+ expect(tester.takeException(), isNull);
+ await tester.pumpAndSettle();
+ expect(tester.takeException(), isNull);
+ });
+
+ testWidgets('can specify a duration', (WidgetTester tester) async {
+ await tester.pumpWidget(Center(
+ child: SizedBox(
+ width: 300,
+ height: 400,
+ child: _boilerplate(
+ child: Center(
+ child: OpenContainer(
+ transitionDuration: const Duration(seconds: 2),
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ return const Text('Closed');
+ },
+ openBuilder: (BuildContext context, VoidCallback action) {
+ return const Text('Open');
+ },
+ ),
+ ),
+ ),
+ ),
+ ));
+
+ expect(find.text('Open'), findsNothing);
+ expect(find.text('Closed'), findsOneWidget);
+
+ await tester.tap(find.text('Closed'));
+ await tester.pump();
+
+ // Jump to the end of the transition.
+ await tester.pump(const Duration(seconds: 2));
+ expect(find.text('Open'), findsOneWidget); // faded in
+ expect(find.text('Closed'), findsOneWidget); // faded out
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsNothing);
+
+ final NavigatorState navigator = tester.state(find.byType(Navigator));
+ navigator.pop();
+ await tester.pump();
+
+ // Jump to the end of the transition.
+ await tester.pump(const Duration(seconds: 2));
+ expect(find.text('Open'), findsOneWidget); // faded out
+ expect(find.text('Closed'), findsOneWidget); // faded in
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text('Open'), findsNothing);
+ expect(find.text('Closed'), findsOneWidget);
+ });
+
+ testWidgets('can specify an open shape', (WidgetTester tester) async {
+ await tester.pumpWidget(Center(
+ child: SizedBox(
+ width: 300,
+ height: 400,
+ child: _boilerplate(
+ child: Center(
+ child: OpenContainer(
+ closedShape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10),
+ ),
+ openShape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(40),
+ ),
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ return const Text('Closed');
+ },
+ openBuilder: (BuildContext context, VoidCallback action) {
+ return const Text('Open');
+ },
+ ),
+ ),
+ ),
+ ),
+ ));
+
+ expect(find.text('Open'), findsNothing);
+ expect(find.text('Closed'), findsOneWidget);
+ final double closedRadius = _getRadius(tester.firstWidget(find.ancestor(
+ of: find.text('Closed'),
+ matching: find.byType(Material),
+ )));
+ expect(closedRadius, 10.0);
+
+ await tester.tap(find.text('Closed'));
+ await tester.pump();
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsOneWidget);
+ final double openingRadius = _getRadius(tester.firstWidget(find.ancestor(
+ of: find.text('Open'),
+ matching: find.byType(Material),
+ )));
+ expect(openingRadius, 10.0);
+
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsOneWidget);
+ final double halfwayRadius = _getRadius(tester.firstWidget(find.ancestor(
+ of: find.text('Open'),
+ matching: find.byType(Material),
+ )));
+ expect(halfwayRadius, greaterThan(10.0));
+ expect(halfwayRadius, lessThan(40.0));
+
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsOneWidget);
+ final double openRadius = _getRadius(tester.firstWidget(find.ancestor(
+ of: find.text('Open'),
+ matching: find.byType(Material),
+ )));
+ expect(openRadius, 40.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text('Closed'), findsNothing);
+ expect(find.text('Open'), findsOneWidget);
+ final double finalRadius = _getRadius(tester.firstWidget(find.ancestor(
+ of: find.text('Open'),
+ matching: find.byType(Material),
+ )));
+ expect(finalRadius, 40.0);
+ });
+
+ testWidgets('Scrim', (WidgetTester tester) async {
+ const ShapeBorder shape = RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(8.0)),
+ );
+
+ await tester.pumpWidget(_boilerplate(
+ child: Center(
+ child: OpenContainer(
+ closedColor: Colors.green,
+ openColor: Colors.blue,
+ closedElevation: 4.0,
+ openElevation: 8.0,
+ closedShape: shape,
+ closedBuilder: (BuildContext context, VoidCallback _) {
+ return const Text('Closed');
+ },
+ openBuilder: (BuildContext context, VoidCallback _) {
+ return const Text('Open');
+ },
+ ),
+ ),
+ ));
+
+ expect(find.text('Closed'), findsOneWidget);
+ expect(find.text('Open'), findsNothing);
+ await tester.tap(find.text('Closed'));
+ await tester.pump();
+
+ expect(_getScrimColor(tester), Colors.transparent);
+
+ await tester.pump(const Duration(milliseconds: 50));
+ final Color halfwayFadeInColor = _getScrimColor(tester);
+ expect(halfwayFadeInColor, isNot(Colors.transparent));
+ expect(halfwayFadeInColor, isNot(Colors.black54));
+
+ // Scrim is done fading in early.
+ await tester.pump(const Duration(milliseconds: 50));
+ expect(_getScrimColor(tester), Colors.black54);
+
+ await tester.pump(const Duration(milliseconds: 200));
+ expect(_getScrimColor(tester), Colors.black54);
+
+ await tester.pumpAndSettle();
+
+ // Close the container
+ final NavigatorState navigator = tester.state(find.byType(Navigator));
+ navigator.pop();
+ await tester.pump();
+
+ expect(_getScrimColor(tester), Colors.black54);
+
+ // Scrim takes longer to fade out (vs. fade in).
+ await tester.pump(const Duration(milliseconds: 200));
+ final Color halfwayFadeOutColor = _getScrimColor(tester);
+ expect(halfwayFadeOutColor, isNot(Colors.transparent));
+ expect(halfwayFadeOutColor, isNot(Colors.black54));
+
+ await tester.pump(const Duration(milliseconds: 100));
+ expect(_getScrimColor(tester), Colors.transparent);
+ });
+
+ testWidgets(
+ 'Container partly offscreen can be opened without crash - vertical',
+ (WidgetTester tester) async {
+ final ScrollController controller =
+ ScrollController(initialScrollOffset: 50);
+ await tester.pumpWidget(Center(
+ child: SizedBox(
+ height: 200,
+ width: 200,
+ child: _boilerplate(
+ child: ListView.builder(
+ cacheExtent: 0,
+ controller: controller,
+ itemBuilder: (BuildContext context, int index) {
+ return OpenContainer(
+ closedBuilder: (BuildContext context, VoidCallback _) {
+ return SizedBox(
+ height: 100,
+ width: 100,
+ child: Text('Closed $index'),
+ );
+ },
+ openBuilder: (BuildContext context, VoidCallback _) {
+ return Text('Open $index');
+ },
+ );
+ },
+ ),
+ ),
+ ),
+ ));
+
+ void expectClosedState() {
+ expect(find.text('Closed 0'), findsOneWidget);
+ expect(find.text('Closed 1'), findsOneWidget);
+ expect(find.text('Closed 2'), findsOneWidget);
+ expect(find.text('Closed 3'), findsNothing);
+
+ expect(find.text('Open 0'), findsNothing);
+ expect(find.text('Open 1'), findsNothing);
+ expect(find.text('Open 2'), findsNothing);
+ expect(find.text('Open 3'), findsNothing);
+ }
+
+ expectClosedState();
+
+ // Open container that's partly visible at top.
+ await tester.tapAt(
+ tester.getBottomRight(find.text('Closed 0')) - const Offset(20, 20),
+ );
+ await tester.pump();
+ await tester.pumpAndSettle();
+ expect(find.text('Closed 0'), findsNothing);
+ expect(find.text('Open 0'), findsOneWidget);
+
+ final NavigatorState navigator = tester.state(find.byType(Navigator));
+ navigator.pop();
+ await tester.pump();
+ await tester.pumpAndSettle();
+ expectClosedState();
+
+ // Open container that's partly visible at bottom.
+ await tester.tapAt(
+ tester.getTopLeft(find.text('Closed 2')) + const Offset(20, 20),
+ );
+ await tester.pump();
+ await tester.pumpAndSettle();
+
+ expect(find.text('Closed 2'), findsNothing);
+ expect(find.text('Open 2'), findsOneWidget);
+ });
+
+ testWidgets(
+ 'Container partly offscreen can be opened without crash - horizontal',
+ (WidgetTester tester) async {
+ final ScrollController controller =
+ ScrollController(initialScrollOffset: 50);
+ await tester.pumpWidget(Center(
+ child: SizedBox(
+ height: 200,
+ width: 200,
+ child: _boilerplate(
+ child: ListView.builder(
+ scrollDirection: Axis.horizontal,
+ cacheExtent: 0,
+ controller: controller,
+ itemBuilder: (BuildContext context, int index) {
+ return OpenContainer(
+ closedBuilder: (BuildContext context, VoidCallback _) {
+ return SizedBox(
+ height: 100,
+ width: 100,
+ child: Text('Closed $index'),
+ );
+ },
+ openBuilder: (BuildContext context, VoidCallback _) {
+ return Text('Open $index');
+ },
+ );
+ },
+ ),
+ ),
+ ),
+ ));
+
+ void expectClosedState() {
+ expect(find.text('Closed 0'), findsOneWidget);
+ expect(find.text('Closed 1'), findsOneWidget);
+ expect(find.text('Closed 2'), findsOneWidget);
+ expect(find.text('Closed 3'), findsNothing);
+
+ expect(find.text('Open 0'), findsNothing);
+ expect(find.text('Open 1'), findsNothing);
+ expect(find.text('Open 2'), findsNothing);
+ expect(find.text('Open 3'), findsNothing);
+ }
+
+ expectClosedState();
+
+ // Open container that's partly visible at left edge.
+ await tester.tapAt(
+ tester.getBottomRight(find.text('Closed 0')) - const Offset(20, 20),
+ );
+ await tester.pump();
+ await tester.pumpAndSettle();
+ expect(find.text('Closed 0'), findsNothing);
+ expect(find.text('Open 0'), findsOneWidget);
+
+ final NavigatorState navigator = tester.state(find.byType(Navigator));
+ navigator.pop();
+ await tester.pump();
+ await tester.pumpAndSettle();
+ expectClosedState();
+
+ // Open container that's partly visible at right edge.
+ await tester.tapAt(
+ tester.getTopLeft(find.text('Closed 2')) + const Offset(20, 20),
+ );
+ await tester.pump();
+ await tester.pumpAndSettle();
+
+ expect(find.text('Closed 2'), findsNothing);
+ expect(find.text('Open 2'), findsOneWidget);
+ });
+
+ testWidgets(
+ 'Container can be dismissed after container widget itself is removed without crash',
+ (WidgetTester tester) async {
+ await tester.pumpWidget(_boilerplate(child: _RemoveOpenContainerExample()));
+
+ expect(find.text('Closed'), findsOneWidget);
+ expect(find.text('Closed', skipOffstage: false), findsOneWidget);
+ expect(find.text('Open'), findsNothing);
+
+ await tester.tap(find.text('Open the container'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Closed'), findsNothing);
+ expect(find.text('Closed', skipOffstage: false), findsOneWidget);
+ expect(find.text('Open'), findsOneWidget);
+
+ await tester.tap(find.text('Remove the container'));
+ await tester.pump();
+
+ expect(find.text('Closed'), findsNothing);
+ expect(find.text('Closed', skipOffstage: false), findsNothing);
+ expect(find.text('Open'), findsOneWidget);
+
+ await tester.tap(find.text('Close the container'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Closed'), findsNothing);
+ expect(find.text('Closed', skipOffstage: false), findsNothing);
+ expect(find.text('Open'), findsNothing);
+ expect(find.text('Container has been removed'), findsOneWidget);
+ });
+
+ testWidgets('onClosed callback is called when container has closed',
+ (WidgetTester tester) async {
+ bool hasClosed = false;
+ final Widget openContainer = OpenContainer(
+ onClosed: (dynamic _) {
+ hasClosed = true;
+ },
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ return GestureDetector(
+ onTap: action,
+ child: const Text('Closed'),
+ );
+ },
+ openBuilder: (BuildContext context, VoidCallback action) {
+ return GestureDetector(
+ onTap: action,
+ child: const Text('Open'),
+ );
+ },
+ );
+
+ await tester.pumpWidget(
+ _boilerplate(child: openContainer),
+ );
+
+ expect(find.text('Open'), findsNothing);
+ expect(find.text('Closed'), findsOneWidget);
+ expect(hasClosed, isFalse);
+
+ await tester.tap(find.text('Closed'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsNothing);
+
+ await tester.tap(find.text('Open'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Open'), findsNothing);
+ expect(find.text('Closed'), findsOneWidget);
+ expect(hasClosed, isTrue);
+ });
+
+ testWidgets(
+ 'onClosed callback receives popped value when container has closed',
+ (WidgetTester tester) async {
+ bool? value = false;
+ final Widget openContainer = OpenContainer<bool>(
+ onClosed: (bool? poppedValue) {
+ value = poppedValue;
+ },
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ return GestureDetector(
+ onTap: action,
+ child: const Text('Closed'),
+ );
+ },
+ openBuilder:
+ (BuildContext context, CloseContainerActionCallback<bool> action) {
+ return GestureDetector(
+ onTap: () => action(returnValue: true),
+ child: const Text('Open'),
+ );
+ },
+ );
+
+ await tester.pumpWidget(
+ _boilerplate(child: openContainer),
+ );
+
+ expect(find.text('Open'), findsNothing);
+ expect(find.text('Closed'), findsOneWidget);
+ expect(value, isFalse);
+
+ await tester.tap(find.text('Closed'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsNothing);
+
+ await tester.tap(find.text('Open'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Open'), findsNothing);
+ expect(find.text('Closed'), findsOneWidget);
+ expect(value, isTrue);
+ });
+
+ testWidgets('closedBuilder has anti-alias clip by default',
+ (WidgetTester tester) async {
+ final GlobalKey closedBuilderKey = GlobalKey();
+ final Widget openContainer = OpenContainer(
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ return Text('Close', key: closedBuilderKey);
+ },
+ openBuilder:
+ (BuildContext context, CloseContainerActionCallback<bool> action) {
+ return const Text('Open');
+ },
+ );
+
+ await tester.pumpWidget(
+ _boilerplate(child: openContainer),
+ );
+
+ final Finder closedBuilderMaterial = find
+ .ancestor(
+ of: find.byKey(closedBuilderKey),
+ matching: find.byType(Material),
+ )
+ .first;
+
+ final Material material = tester.widget<Material>(closedBuilderMaterial);
+ expect(material.clipBehavior, Clip.antiAlias);
+ });
+
+ testWidgets('closedBuilder has no clip', (WidgetTester tester) async {
+ final GlobalKey closedBuilderKey = GlobalKey();
+ final Widget openContainer = OpenContainer(
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ return Text('Close', key: closedBuilderKey);
+ },
+ openBuilder:
+ (BuildContext context, CloseContainerActionCallback<bool> action) {
+ return const Text('Open');
+ },
+ clipBehavior: Clip.none,
+ );
+
+ await tester.pumpWidget(
+ _boilerplate(child: openContainer),
+ );
+
+ final Finder closedBuilderMaterial = find
+ .ancestor(
+ of: find.byKey(closedBuilderKey),
+ matching: find.byType(Material),
+ )
+ .first;
+
+ final Material material = tester.widget<Material>(closedBuilderMaterial);
+ expect(material.clipBehavior, Clip.none);
+ });
+
+ Widget _createRootNavigatorTest({
+ required Key appKey,
+ required Key nestedNavigatorKey,
+ required bool useRootNavigator,
+ }) {
+ return Center(
+ child: SizedBox(
+ width: 100,
+ height: 100,
+ child: MaterialApp(
+ key: appKey,
+ // a nested navigator
+ home: Center(
+ child: SizedBox(
+ width: 50,
+ height: 50,
+ child: Navigator(
+ key: nestedNavigatorKey,
+ onGenerateRoute: (RouteSettings route) {
+ return MaterialPageRoute<dynamic>(
+ settings: route,
+ builder: (BuildContext context) {
+ return OpenContainer(
+ useRootNavigator: useRootNavigator,
+ closedBuilder: (BuildContext context, _) {
+ return const Text('Closed');
+ },
+ openBuilder: (BuildContext context, _) {
+ return const Text('Opened');
+ },
+ );
+ },
+ );
+ },
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ testWidgets(
+ 'Verify that "useRootNavigator: false" uses the correct navigator',
+ (WidgetTester tester) async {
+ const Key appKey = Key('App');
+ const Key nestedNavigatorKey = Key('Nested Navigator');
+
+ await tester.pumpWidget(_createRootNavigatorTest(
+ appKey: appKey,
+ nestedNavigatorKey: nestedNavigatorKey,
+ useRootNavigator: false));
+
+ await tester.tap(find.text('Closed'));
+ await tester.pumpAndSettle();
+
+ expect(
+ find.descendant(of: find.byKey(appKey), matching: find.text('Opened')),
+ findsOneWidget);
+
+ expect(
+ find.descendant(
+ of: find.byKey(nestedNavigatorKey), matching: find.text('Opened')),
+ findsOneWidget);
+ });
+
+ testWidgets('Verify that "useRootNavigator: true" uses the correct navigator',
+ (WidgetTester tester) async {
+ const Key appKey = Key('App');
+ const Key nestedNavigatorKey = Key('Nested Navigator');
+
+ await tester.pumpWidget(_createRootNavigatorTest(
+ appKey: appKey,
+ nestedNavigatorKey: nestedNavigatorKey,
+ useRootNavigator: true));
+
+ await tester.tap(find.text('Closed'));
+ await tester.pumpAndSettle();
+
+ expect(
+ find.descendant(of: find.byKey(appKey), matching: find.text('Opened')),
+ findsOneWidget);
+
+ expect(
+ find.descendant(
+ of: find.byKey(nestedNavigatorKey), matching: find.text('Opened')),
+ findsNothing);
+ });
+
+ testWidgets('Verify correct opened size when "useRootNavigator: false"',
+ (WidgetTester tester) async {
+ const Key appKey = Key('App');
+ const Key nestedNavigatorKey = Key('Nested Navigator');
+
+ await tester.pumpWidget(_createRootNavigatorTest(
+ appKey: appKey,
+ nestedNavigatorKey: nestedNavigatorKey,
+ useRootNavigator: false));
+
+ await tester.tap(find.text('Closed'));
+ await tester.pumpAndSettle();
+
+ expect(tester.getSize(find.text('Opened')),
+ equals(tester.getSize(find.byKey(nestedNavigatorKey))));
+ });
+
+ testWidgets('Verify correct opened size when "useRootNavigator: true"',
+ (WidgetTester tester) async {
+ const Key appKey = Key('App');
+ const Key nestedNavigatorKey = Key('Nested Navigator');
+
+ await tester.pumpWidget(_createRootNavigatorTest(
+ appKey: appKey,
+ nestedNavigatorKey: nestedNavigatorKey,
+ useRootNavigator: true));
+
+ await tester.tap(find.text('Closed'));
+ await tester.pumpAndSettle();
+
+ expect(tester.getSize(find.text('Opened')),
+ equals(tester.getSize(find.byKey(appKey))));
+ });
+
+ testWidgets(
+ 'Verify routeSettings passed to Navigator',
+ (WidgetTester tester) async {
+ const RouteSettings routeSettings = RouteSettings(
+ name: 'route-name',
+ arguments: 'arguments',
+ );
+
+ final Widget openContainer = OpenContainer(
+ routeSettings: routeSettings,
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ return GestureDetector(
+ onTap: action,
+ child: const Text('Closed'),
+ );
+ },
+ openBuilder: (BuildContext context, VoidCallback action) {
+ return GestureDetector(
+ onTap: action,
+ child: const Text('Open'),
+ );
+ },
+ );
+
+ await tester.pumpWidget(_boilerplate(child: openContainer));
+
+ // Open the container
+ await tester.tap(find.text('Closed'));
+ await tester.pumpAndSettle();
+
+ // Expect the last route pushed to the navigator to contain RouteSettings
+ // equal to the RouteSettings passed to the OpenContainer
+ final ModalRoute<dynamic> modalRoute = ModalRoute.of(
+ tester.element(find.text('Open')),
+ )!;
+ expect(modalRoute.settings, routeSettings);
+ },
+ );
+
+ // Regression test for https://github.com/flutter/flutter/issues/72238.
+ testWidgets(
+ 'OpenContainer\'s source widget is visible in closed container route if '
+ 'open container route is pushed from not using the OpenContainer itself',
+ (WidgetTester tester) async {
+ final Widget openContainer = OpenContainer(
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ return GestureDetector(
+ onTap: action,
+ child: const Text('Closed'),
+ );
+ },
+ openBuilder: (BuildContext context, VoidCallback action) {
+ return GestureDetector(
+ onTap: action,
+ child: const Text('Open'),
+ );
+ },
+ );
+ await tester.pumpWidget(_boilerplate(child: openContainer));
+ expect(_getOpacity(tester, 'Closed'), 1.0);
+
+ // Open the container
+ await tester.tap(find.text('Closed'));
+ await tester.pumpAndSettle();
+
+ final Element container = tester.element(
+ find.byType(OpenContainer, skipOffstage: false),
+ );
+ // Replace the open container route.
+ Navigator.pushReplacement<void, void>(container,
+ MaterialPageRoute<void>(builder: (_) => const Placeholder()));
+ await tester.pumpAndSettle();
+ // Go back to the main page and verify the closed builder is showed.
+ Navigator.pop(container);
+ await tester.pumpAndSettle();
+
+ expect(_getOpacity(tester, 'Closed'), 1.0);
+ },
+ );
+}
+
+Color _getScrimColor(WidgetTester tester) {
+ return tester.widget<ColoredBox>(find.byType(ColoredBox)).color;
+}
+
+void _expectMaterialPropertiesHaveAdvanced({
+ required _TrackedData biggerMaterial,
+ required _TrackedData smallerMaterial,
+ required WidgetTester tester,
+}) {
+ expect(
+ biggerMaterial.material.elevation,
+ greaterThan(smallerMaterial.material.elevation),
+ );
+ expect(biggerMaterial.radius, lessThan(smallerMaterial.radius));
+ expect(biggerMaterial.rect.height, greaterThan(smallerMaterial.rect.height));
+ expect(biggerMaterial.rect.width, greaterThan(smallerMaterial.rect.width));
+ expect(biggerMaterial.rect.top, lessThan(smallerMaterial.rect.top));
+ expect(biggerMaterial.rect.left, lessThan(smallerMaterial.rect.left));
+}
+
+double _getOpacity(WidgetTester tester, String label) {
+ final Opacity widget = tester.firstWidget(find.ancestor(
+ of: find.text(label),
+ matching: find.byType(Opacity),
+ ));
+ return widget.opacity;
+}
+
+class _TrackedData {
+ _TrackedData(this.material, this.rect);
+
+ final Material material;
+ final Rect rect;
+
+ double get radius => _getRadius(material);
+}
+
+double _getRadius(Material material) {
+ final RoundedRectangleBorder? shape =
+ material.shape as RoundedRectangleBorder?;
+ if (shape == null) {
+ return 0.0;
+ }
+ final BorderRadius radius = shape.borderRadius as BorderRadius;
+ return radius.topRight.x;
+}
+
+Widget _boilerplate({required Widget child}) {
+ return MaterialApp(
+ home: Scaffold(
+ body: child,
+ ),
+ );
+}
+
+class _SizableContainer extends StatefulWidget {
+ const _SizableContainer({required this.initialSize, required this.child});
+
+ final double initialSize;
+ final Widget child;
+
+ @override
+ State<_SizableContainer> createState() => _SizableContainerState();
+}
+
+class _SizableContainerState extends State<_SizableContainer> {
+ @override
+ void initState() {
+ super.initState();
+ _size = widget.initialSize;
+ }
+
+ double get size => _size;
+ late double _size;
+ set size(double value) {
+ if (value == _size) {
+ return;
+ }
+ setState(() {
+ _size = value;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ height: size,
+ width: size,
+ child: widget.child,
+ );
+ }
+}
+
+class _RemoveOpenContainerExample extends StatefulWidget {
+ @override
+ __RemoveOpenContainerExampleState createState() =>
+ __RemoveOpenContainerExampleState();
+}
+
+class __RemoveOpenContainerExampleState
+ extends State<_RemoveOpenContainerExample> {
+ bool removeOpenContainerWidget = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return removeOpenContainerWidget
+ ? const Text('Container has been removed')
+ : OpenContainer(
+ closedBuilder: (BuildContext context, VoidCallback action) =>
+ Column(
+ children: <Widget>[
+ const Text('Closed'),
+ ElevatedButton(
+ onPressed: action,
+ child: const Text('Open the container'),
+ ),
+ ],
+ ),
+ openBuilder: (BuildContext context, VoidCallback action) => Column(
+ children: <Widget>[
+ const Text('Open'),
+ ElevatedButton(
+ onPressed: action,
+ child: const Text('Close the container'),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ setState(() {
+ removeOpenContainerWidget = true;
+ });
+ },
+ child: const Text('Remove the container')),
+ ],
+ ),
+ );
+ }
+}
+
+class DummyStatefulWidget extends StatefulWidget {
+ const DummyStatefulWidget({Key? key}) : super(key: key);
+
+ @override
+ State<StatefulWidget> createState() => DummyState();
+}
+
+class DummyState extends State<DummyStatefulWidget> {
+ @override
+ Widget build(BuildContext context) => const SizedBox.expand();
+}
diff --git a/packages/animations/test/page_transition_switcher_test.dart b/packages/animations/test/page_transition_switcher_test.dart
new file mode 100644
index 0000000..ad3965e
--- /dev/null
+++ b/packages/animations/test/page_transition_switcher_test.dart
@@ -0,0 +1,647 @@
+// 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:animations/animations.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ testWidgets('transitions in a new child.', (WidgetTester tester) async {
+ final UniqueKey containerOne = UniqueKey();
+ final UniqueKey containerTwo = UniqueKey();
+ final UniqueKey containerThree = UniqueKey();
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: containerOne, color: const Color(0x00000000)),
+ transitionBuilder: _transitionBuilder,
+ ),
+ );
+
+ Map<Key, double> _primaryAnimation =
+ _getPrimaryAnimation(<Key>[containerOne], tester);
+ Map<Key, double> _secondaryAnimation =
+ _getSecondaryAnimation(<Key>[containerOne], tester);
+ expect(_primaryAnimation[containerOne], equals(1.0));
+ expect(_secondaryAnimation[containerOne], equals(0.0));
+
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: containerTwo, color: const Color(0xff000000)),
+ transitionBuilder: _transitionBuilder,
+ ),
+ );
+ await tester.pump(const Duration(milliseconds: 40));
+
+ _primaryAnimation =
+ _getPrimaryAnimation(<Key>[containerOne, containerTwo], tester);
+ _secondaryAnimation =
+ _getSecondaryAnimation(<Key>[containerOne, containerTwo], tester);
+ // Secondary is running for outgoing widget.
+ expect(_primaryAnimation[containerOne], equals(1.0));
+ expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.4));
+ // Primary is running for incoming widget.
+ expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.4));
+ expect(_secondaryAnimation[containerTwo], equals(0.0));
+
+ // Container one is underneath container two
+ final Container container = tester.firstWidget(find.byType(Container));
+ expect(container.key, containerOne);
+
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: containerThree, color: const Color(0xffff0000)),
+ transitionBuilder: _transitionBuilder,
+ ),
+ );
+ await tester.pump(const Duration(milliseconds: 20));
+
+ _primaryAnimation = _getPrimaryAnimation(
+ <Key>[containerOne, containerTwo, containerThree], tester);
+ _secondaryAnimation = _getSecondaryAnimation(
+ <Key>[containerOne, containerTwo, containerThree], tester);
+ expect(_primaryAnimation[containerOne], equals(1.0));
+ expect(_secondaryAnimation[containerOne], equals(0.6));
+ expect(_primaryAnimation[containerTwo], equals(0.6));
+ expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.2));
+ expect(_primaryAnimation[containerThree], moreOrLessEquals(0.2));
+ expect(_secondaryAnimation[containerThree], equals(0.0));
+ await tester.pumpAndSettle();
+ });
+
+ testWidgets('transitions in a new child in reverse.',
+ (WidgetTester tester) async {
+ final UniqueKey containerOne = UniqueKey();
+ final UniqueKey containerTwo = UniqueKey();
+ final UniqueKey containerThree = UniqueKey();
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: containerOne, color: const Color(0x00000000)),
+ transitionBuilder: _transitionBuilder,
+ reverse: true,
+ ),
+ );
+
+ Map<Key, double> _primaryAnimation =
+ _getPrimaryAnimation(<Key>[containerOne], tester);
+ Map<Key, double> _secondaryAnimation =
+ _getSecondaryAnimation(<Key>[containerOne], tester);
+ expect(_primaryAnimation[containerOne], equals(1.0));
+ expect(_secondaryAnimation[containerOne], equals(0.0));
+
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: containerTwo, color: const Color(0xff000000)),
+ transitionBuilder: _transitionBuilder,
+ reverse: true,
+ ),
+ );
+ await tester.pump(const Duration(milliseconds: 40));
+
+ _primaryAnimation =
+ _getPrimaryAnimation(<Key>[containerOne, containerTwo], tester);
+ _secondaryAnimation =
+ _getSecondaryAnimation(<Key>[containerOne, containerTwo], tester);
+ // Primary is running forward for outgoing widget.
+ expect(_primaryAnimation[containerOne], moreOrLessEquals(0.6));
+ expect(_secondaryAnimation[containerOne], equals(0.0));
+ // Secondary is running forward for incoming widget.
+ expect(_primaryAnimation[containerTwo], equals(1.0));
+ expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.6));
+
+ // Container two two is underneath container one.
+ final Container container = tester.firstWidget(find.byType(Container));
+ expect(container.key, containerTwo);
+
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: containerThree, color: const Color(0xffff0000)),
+ transitionBuilder: _transitionBuilder,
+ reverse: true,
+ ),
+ );
+ await tester.pump(const Duration(milliseconds: 20));
+
+ _primaryAnimation = _getPrimaryAnimation(
+ <Key>[containerOne, containerTwo, containerThree], tester);
+ _secondaryAnimation = _getSecondaryAnimation(
+ <Key>[containerOne, containerTwo, containerThree], tester);
+ expect(_primaryAnimation[containerOne], equals(0.4));
+ expect(_secondaryAnimation[containerOne], equals(0.0));
+ expect(_primaryAnimation[containerTwo], equals(0.8));
+ expect(_secondaryAnimation[containerTwo], equals(0.4));
+ expect(_primaryAnimation[containerThree], equals(1.0));
+ expect(_secondaryAnimation[containerThree], equals(0.8));
+ await tester.pumpAndSettle();
+ });
+
+ testWidgets('switch from forward to reverse', (WidgetTester tester) async {
+ final UniqueKey containerOne = UniqueKey();
+ final UniqueKey containerTwo = UniqueKey();
+ final UniqueKey containerThree = UniqueKey();
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: containerOne, color: const Color(0x00000000)),
+ transitionBuilder: _transitionBuilder,
+ ),
+ );
+
+ Map<Key, double> _primaryAnimation =
+ _getPrimaryAnimation(<Key>[containerOne], tester);
+ Map<Key, double> _secondaryAnimation =
+ _getSecondaryAnimation(<Key>[containerOne], tester);
+ expect(_primaryAnimation[containerOne], equals(1.0));
+ expect(_secondaryAnimation[containerOne], equals(0.0));
+
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: containerTwo, color: const Color(0xff000000)),
+ transitionBuilder: _transitionBuilder,
+ ),
+ );
+ await tester.pump(const Duration(milliseconds: 40));
+
+ _primaryAnimation =
+ _getPrimaryAnimation(<Key>[containerOne, containerTwo], tester);
+ _secondaryAnimation =
+ _getSecondaryAnimation(<Key>[containerOne, containerTwo], tester);
+ expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.4));
+ expect(_primaryAnimation[containerOne], equals(1.0));
+ expect(_secondaryAnimation[containerTwo], equals(0.0));
+ expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.4));
+
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: containerThree, color: const Color(0xffff0000)),
+ transitionBuilder: _transitionBuilder,
+ reverse: true,
+ ),
+ );
+ await tester.pump(const Duration(milliseconds: 20));
+
+ _primaryAnimation = _getPrimaryAnimation(
+ <Key>[containerOne, containerTwo, containerThree], tester);
+ _secondaryAnimation = _getSecondaryAnimation(
+ <Key>[containerOne, containerTwo, containerThree], tester);
+ expect(_secondaryAnimation[containerOne], equals(0.6));
+ expect(_primaryAnimation[containerOne], equals(1.0));
+ expect(_secondaryAnimation[containerTwo], equals(0.0));
+ expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.2));
+ expect(_secondaryAnimation[containerThree], equals(0.8));
+ expect(_primaryAnimation[containerThree], equals(1.0));
+ await tester.pumpAndSettle();
+ });
+
+ testWidgets('switch from reverse to forward.', (WidgetTester tester) async {
+ final UniqueKey containerOne = UniqueKey();
+ final UniqueKey containerTwo = UniqueKey();
+ final UniqueKey containerThree = UniqueKey();
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: containerOne, color: const Color(0x00000000)),
+ transitionBuilder: _transitionBuilder,
+ reverse: true,
+ ),
+ );
+
+ Map<Key, double> _primaryAnimation =
+ _getPrimaryAnimation(<Key>[containerOne], tester);
+ Map<Key, double> _secondaryAnimation =
+ _getSecondaryAnimation(<Key>[containerOne], tester);
+ expect(_primaryAnimation[containerOne], equals(1.0));
+ expect(_secondaryAnimation[containerOne], equals(0.0));
+
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: containerTwo, color: const Color(0xff000000)),
+ transitionBuilder: _transitionBuilder,
+ reverse: true,
+ ),
+ );
+ await tester.pump(const Duration(milliseconds: 40));
+
+ _primaryAnimation =
+ _getPrimaryAnimation(<Key>[containerOne, containerTwo], tester);
+ _secondaryAnimation =
+ _getSecondaryAnimation(<Key>[containerOne, containerTwo], tester);
+ // Primary is running in reverse for outgoing widget.
+ expect(_primaryAnimation[containerOne], moreOrLessEquals(0.6));
+ expect(_secondaryAnimation[containerOne], equals(0.0));
+ // Secondary is running in reverse for incoming widget.
+ expect(_primaryAnimation[containerTwo], equals(1.0));
+ expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.6));
+
+ // Container two is underneath container one.
+ final Container container = tester.firstWidget(find.byType(Container));
+ expect(container.key, containerTwo);
+
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: containerThree, color: const Color(0xffff0000)),
+ transitionBuilder: _transitionBuilder,
+ reverse: false,
+ ),
+ );
+ await tester.pump(const Duration(milliseconds: 20));
+
+ // Container one is expected to continue running its primary animation in
+ // reverse since it is exiting. Container two's secondary animation switches
+ // from running its secondary animation in reverse to running forwards since
+ // it should now be exiting underneath container three. Container three's
+ // primary animation should be running forwards since it is entering above
+ // container two.
+ _primaryAnimation = _getPrimaryAnimation(
+ <Key>[containerOne, containerTwo, containerThree], tester);
+ _secondaryAnimation = _getSecondaryAnimation(
+ <Key>[containerOne, containerTwo, containerThree], tester);
+ expect(_primaryAnimation[containerOne], equals(0.4));
+ expect(_secondaryAnimation[containerOne], equals(0.0));
+ expect(_primaryAnimation[containerTwo], equals(1.0));
+ expect(_secondaryAnimation[containerTwo], equals(0.8));
+ expect(_primaryAnimation[containerThree], moreOrLessEquals(0.2));
+ expect(_secondaryAnimation[containerThree], equals(0.0));
+ await tester.pumpAndSettle();
+ });
+
+ testWidgets('using custom layout', (WidgetTester tester) async {
+ Widget newLayoutBuilder(List<Widget> activeEntries) {
+ return Column(
+ children: activeEntries,
+ );
+ }
+
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(color: const Color(0x00000000)),
+ transitionBuilder: _transitionBuilder,
+ layoutBuilder: newLayoutBuilder,
+ ),
+ );
+
+ expect(find.byType(Column), findsOneWidget);
+ });
+
+ testWidgets("doesn't transition in a new child of the same type.",
+ (WidgetTester tester) async {
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(color: const Color(0x00000000)),
+ transitionBuilder: _transitionBuilder,
+ ),
+ );
+
+ expect(find.byType(FadeTransition), findsOneWidget);
+ expect(find.byType(ScaleTransition), findsOneWidget);
+ FadeTransition fade = tester.firstWidget(find.byType(FadeTransition));
+ ScaleTransition scale = tester.firstWidget(find.byType(ScaleTransition));
+ expect(fade.opacity.value, equals(1.0));
+ expect(scale.scale.value, equals(1.0));
+
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(color: const Color(0xff000000)),
+ transitionBuilder: _transitionBuilder,
+ ),
+ );
+
+ await tester.pump(const Duration(milliseconds: 50));
+ expect(find.byType(FadeTransition), findsOneWidget);
+ expect(find.byType(ScaleTransition), findsOneWidget);
+ fade = tester.firstWidget(find.byType(FadeTransition));
+ scale = tester.firstWidget(find.byType(ScaleTransition));
+ expect(fade.opacity.value, equals(1.0));
+ expect(scale.scale.value, equals(1.0));
+ await tester.pumpAndSettle();
+ });
+
+ testWidgets('handles null children.', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ const PageTransitionSwitcher(
+ duration: Duration(milliseconds: 100),
+ child: null,
+ transitionBuilder: _transitionBuilder,
+ ),
+ );
+
+ expect(find.byType(FadeTransition), findsNothing);
+ expect(find.byType(ScaleTransition), findsNothing);
+
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(color: const Color(0xff000000)),
+ transitionBuilder: _transitionBuilder,
+ ),
+ );
+
+ await tester.pump(const Duration(milliseconds: 40));
+ expect(find.byType(FadeTransition), findsOneWidget);
+ expect(find.byType(ScaleTransition), findsOneWidget);
+ FadeTransition fade = tester.firstWidget(find.byType(FadeTransition));
+ ScaleTransition scale = tester.firstWidget(find.byType(ScaleTransition));
+ expect(fade.opacity.value, equals(1.0));
+ expect(scale.scale.value, moreOrLessEquals(0.4));
+ await tester.pumpAndSettle(); // finish transitions.
+
+ await tester.pumpWidget(
+ const PageTransitionSwitcher(
+ duration: Duration(milliseconds: 100),
+ child: null,
+ transitionBuilder: _transitionBuilder,
+ ),
+ );
+
+ await tester.pump(const Duration(milliseconds: 50));
+ expect(find.byType(FadeTransition), findsOneWidget);
+ expect(find.byType(ScaleTransition), findsOneWidget);
+ fade = tester.firstWidget(find.byType(FadeTransition));
+ scale = tester.firstWidget(find.byType(ScaleTransition));
+ expect(fade.opacity.value, equals(0.5));
+ expect(scale.scale.value, equals(1.0));
+ await tester.pumpAndSettle();
+ });
+
+ testWidgets("doesn't start any animations after dispose.",
+ (WidgetTester tester) async {
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: UniqueKey(), color: const Color(0xff000000)),
+ transitionBuilder: _transitionBuilder,
+ ),
+ );
+
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: UniqueKey(), color: const Color(0xff000000)),
+ transitionBuilder: _transitionBuilder,
+ ),
+ );
+ await tester.pump(const Duration(milliseconds: 50));
+
+ expect(find.byType(FadeTransition), findsNWidgets(2));
+ expect(find.byType(ScaleTransition), findsNWidgets(2));
+ final FadeTransition fade = tester.firstWidget(find.byType(FadeTransition));
+ final ScaleTransition scale =
+ tester.firstWidget(find.byType(ScaleTransition));
+ expect(fade.opacity.value, equals(0.5));
+ expect(scale.scale.value, equals(1.0));
+
+ // Change the widget tree in the middle of the animation.
+ await tester.pumpWidget(Container(color: const Color(0xffff0000)));
+ expect(await tester.pumpAndSettle(const Duration(milliseconds: 100)),
+ equals(1));
+ });
+
+ testWidgets("doesn't reset state of the children in transitions.",
+ (WidgetTester tester) async {
+ final UniqueKey statefulOne = UniqueKey();
+ final UniqueKey statefulTwo = UniqueKey();
+ final UniqueKey statefulThree = UniqueKey();
+
+ StatefulTestWidgetState.generation = 0;
+
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: StatefulTestWidget(key: statefulOne),
+ transitionBuilder: _transitionBuilder,
+ ),
+ );
+
+ Map<Key, double> _primaryAnimation =
+ _getPrimaryAnimation(<Key>[statefulOne], tester);
+ Map<Key, double> _secondaryAnimation =
+ _getSecondaryAnimation(<Key>[statefulOne], tester);
+ expect(_primaryAnimation[statefulOne], equals(1.0));
+ expect(_secondaryAnimation[statefulOne], equals(0.0));
+ expect(StatefulTestWidgetState.generation, equals(1));
+
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: StatefulTestWidget(key: statefulTwo),
+ transitionBuilder: _transitionBuilder,
+ ),
+ );
+
+ await tester.pump(const Duration(milliseconds: 50));
+ expect(find.byType(FadeTransition), findsNWidgets(2));
+ _primaryAnimation =
+ _getPrimaryAnimation(<Key>[statefulOne, statefulTwo], tester);
+ _secondaryAnimation =
+ _getSecondaryAnimation(<Key>[statefulOne, statefulTwo], tester);
+ expect(_primaryAnimation[statefulTwo], equals(0.5));
+ expect(_secondaryAnimation[statefulTwo], equals(0.0));
+ expect(StatefulTestWidgetState.generation, equals(2));
+
+ await tester.pumpWidget(
+ PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: StatefulTestWidget(key: statefulThree),
+ transitionBuilder: _transitionBuilder,
+ ),
+ );
+
+ await tester.pump(const Duration(milliseconds: 10));
+ expect(StatefulTestWidgetState.generation, equals(3));
+ await tester.pumpAndSettle();
+ expect(StatefulTestWidgetState.generation, equals(3));
+ });
+
+ testWidgets('updates widgets without animating if they are isomorphic.',
+ (WidgetTester tester) async {
+ Future<void> pumpChild(Widget child) async {
+ return tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.rtl,
+ child: PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: child,
+ transitionBuilder: _transitionBuilder,
+ ),
+ ),
+ );
+ }
+
+ await pumpChild(const Text('1'));
+ await tester.pump(const Duration(milliseconds: 10));
+ FadeTransition fade = tester.widget(find.byType(FadeTransition));
+ ScaleTransition scale = tester.widget(find.byType(ScaleTransition));
+ expect(fade.opacity.value, equals(1.0));
+ expect(scale.scale.value, equals(1.0));
+ expect(find.text('1'), findsOneWidget);
+ expect(find.text('2'), findsNothing);
+ await pumpChild(const Text('2'));
+ fade = tester.widget(find.byType(FadeTransition));
+ scale = tester.widget(find.byType(ScaleTransition));
+ await tester.pump(const Duration(milliseconds: 20));
+ expect(fade.opacity.value, equals(1.0));
+ expect(scale.scale.value, equals(1.0));
+ expect(find.text('1'), findsNothing);
+ expect(find.text('2'), findsOneWidget);
+ });
+
+ testWidgets(
+ 'updates previous child transitions if the transitionBuilder changes.',
+ (WidgetTester tester) async {
+ final UniqueKey containerOne = UniqueKey();
+ final UniqueKey containerTwo = UniqueKey();
+ final UniqueKey containerThree = UniqueKey();
+
+ // Insert three unique children so that we have some previous children.
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: containerOne, color: const Color(0xFFFF0000)),
+ transitionBuilder: _transitionBuilder,
+ ),
+ ),
+ );
+
+ await tester.pump(const Duration(milliseconds: 10));
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: containerTwo, color: const Color(0xFF00FF00)),
+ transitionBuilder: _transitionBuilder,
+ ),
+ ),
+ );
+
+ await tester.pump(const Duration(milliseconds: 10));
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: containerThree, color: const Color(0xFF0000FF)),
+ transitionBuilder: _transitionBuilder,
+ ),
+ ),
+ );
+
+ await tester.pump(const Duration(milliseconds: 10));
+
+ expect(find.byType(FadeTransition), findsNWidgets(3));
+ expect(find.byType(ScaleTransition), findsNWidgets(3));
+ expect(find.byType(SlideTransition), findsNothing);
+ expect(find.byType(SizeTransition), findsNothing);
+
+ Widget newTransitionBuilder(
+ Widget child, Animation<double> primary, Animation<double> secondary) {
+ return SlideTransition(
+ position: Tween<Offset>(begin: Offset.zero, end: const Offset(20, 30))
+ .animate(primary),
+ child: SizeTransition(
+ sizeFactor: Tween<double>(begin: 10, end: 0.0).animate(secondary),
+ child: child,
+ ),
+ );
+ }
+
+ // Now set a new transition builder and make sure all the previous
+ // transitions are replaced.
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: PageTransitionSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: Container(key: containerThree, color: const Color(0x00000000)),
+ transitionBuilder: newTransitionBuilder,
+ ),
+ ),
+ );
+
+ await tester.pump(const Duration(milliseconds: 10));
+
+ expect(find.byType(FadeTransition), findsNothing);
+ expect(find.byType(ScaleTransition), findsNothing);
+ expect(find.byType(SlideTransition), findsNWidgets(3));
+ expect(find.byType(SizeTransition), findsNWidgets(3));
+ });
+}
+
+class StatefulTestWidget extends StatefulWidget {
+ const StatefulTestWidget({Key? key}) : super(key: key);
+
+ @override
+ StatefulTestWidgetState createState() => StatefulTestWidgetState();
+}
+
+class StatefulTestWidgetState extends State<StatefulTestWidget> {
+ StatefulTestWidgetState();
+ static int generation = 0;
+
+ @override
+ void initState() {
+ super.initState();
+ generation++;
+ }
+
+ @override
+ Widget build(BuildContext context) => Container();
+}
+
+Widget _transitionBuilder(
+ Widget child, Animation<double> primary, Animation<double> secondary) {
+ return ScaleTransition(
+ scale: Tween<double>(begin: 0.0, end: 1.0).animate(primary),
+ child: FadeTransition(
+ opacity: Tween<double>(begin: 1.0, end: 0.0).animate(secondary),
+ child: child,
+ ),
+ );
+}
+
+Map<Key, double> _getSecondaryAnimation(List<Key> keys, WidgetTester tester) {
+ expect(find.byType(FadeTransition), findsNWidgets(keys.length));
+ final Map<Key, double> result = <Key, double>{};
+ for (final Key key in keys) {
+ final FadeTransition transition = tester.firstWidget(
+ find.ancestor(
+ of: find.byKey(key),
+ matching: find.byType(FadeTransition),
+ ),
+ );
+ result[key] = 1.0 - transition.opacity.value;
+ }
+ return result;
+}
+
+Map<Key, double> _getPrimaryAnimation(List<Key> keys, WidgetTester tester) {
+ expect(find.byType(ScaleTransition), findsNWidgets(keys.length));
+ final Map<Key, double> result = <Key, double>{};
+ for (final Key key in keys) {
+ final ScaleTransition transition = tester.firstWidget(
+ find.ancestor(
+ of: find.byKey(key),
+ matching: find.byType(ScaleTransition),
+ ),
+ );
+ result[key] = transition.scale.value;
+ }
+ return result;
+}
diff --git a/packages/animations/test/shared_axis_transition_test.dart b/packages/animations/test/shared_axis_transition_test.dart
new file mode 100644
index 0000000..03ac704
--- /dev/null
+++ b/packages/animations/test/shared_axis_transition_test.dart
@@ -0,0 +1,1951 @@
+// 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:animations/src/shared_axis_transition.dart';
+import 'package:flutter/animation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/widgets.dart';
+import 'package:vector_math/vector_math_64.dart' hide Colors;
+
+void main() {
+ group('SharedAxisTransitionType.horizontal', () {
+ testWidgets(
+ 'SharedAxisPageTransitionsBuilder builds a SharedAxisTransition',
+ (WidgetTester tester) async {
+ final AnimationController animation = AnimationController(
+ vsync: const TestVSync(),
+ );
+ final AnimationController secondaryAnimation = AnimationController(
+ vsync: const TestVSync(),
+ );
+
+ await tester.pumpWidget(
+ const SharedAxisPageTransitionsBuilder(
+ transitionType: SharedAxisTransitionType.horizontal,
+ ).buildTransitions<void>(
+ null,
+ null,
+ animation,
+ secondaryAnimation,
+ const Placeholder(),
+ ),
+ );
+
+ expect(find.byType(SharedAxisTransition), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'SharedAxisTransition runs forward',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ transitionType: SharedAxisTransitionType.horizontal,
+ ),
+ );
+
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ 0.0,
+ );
+ expect(_getOpacity(bottomRoute, tester), 1.0);
+ expect(find.text(topRoute), findsNothing);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+ await tester.pump();
+
+ // Bottom route is not offset and fully visible.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ 0.0,
+ );
+ expect(_getOpacity(bottomRoute, tester), 1.0);
+ // Top route is offset to the right by 30.0 pixels
+ // and not visible yet.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ 30.0,
+ );
+ expect(_getOpacity(topRoute, tester), 0.0);
+
+ // Jump 3/10ths of the way through the transition, bottom route
+ // should be be completely faded out while the top route
+ // is also completely faded out.
+ // Transition time: 300ms, 3/10 * 300ms = 90ms
+ await tester.pump(const Duration(milliseconds: 90));
+
+ // Bottom route is now invisible
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ // Top route is still invisible, but moving towards the left.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getOpacity(topRoute, tester), 0.0);
+ double? topOffset = _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ );
+ expect(topOffset, lessThan(30.0));
+ expect(topOffset, greaterThan(0.0));
+
+ // Jump to the middle of fading in
+ await tester.pump(const Duration(milliseconds: 90));
+ // Bottom route is still invisible
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ // Top route is fading in
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getOpacity(topRoute, tester), greaterThan(0));
+ expect(_getOpacity(topRoute, tester), lessThan(1.0));
+ topOffset = _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ );
+ expect(topOffset, greaterThan(0.0));
+ expect(topOffset, lessThan(30.0));
+
+ // Jump to the end of the transition
+ await tester.pump(const Duration(milliseconds: 120));
+ // Bottom route is not visible.
+ expect(find.text(bottomRoute), findsOneWidget);
+
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ -30.0,
+ );
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ // Top route has no offset and is visible.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ 0.0,
+ );
+ expect(_getOpacity(topRoute, tester), 1.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text(bottomRoute), findsNothing);
+ expect(find.text(topRoute), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'SharedAxisTransition runs in reverse',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ transitionType: SharedAxisTransitionType.horizontal,
+ ),
+ );
+
+ navigator.currentState!.pushNamed('/a');
+ await tester.pumpAndSettle();
+
+ expect(find.text(topRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ 0.0,
+ );
+ expect(_getOpacity(topRoute, tester), 1.0);
+ expect(find.text(bottomRoute), findsNothing);
+
+ navigator.currentState!.pop();
+ await tester.pump();
+
+ // Top route is is not offset and fully visible.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ 0.0,
+ );
+ expect(_getOpacity(topRoute, tester), 1.0);
+ // Bottom route is offset to the left and is not visible yet.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ -30.0,
+ );
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+
+ // Jump 3/10ths of the way through the transition, bottom route
+ // should be be completely faded out while the top route
+ // is also completely faded out.
+ // Transition time: 300ms, 3/10 * 300ms = 90ms
+ await tester.pump(const Duration(milliseconds: 90));
+
+ // Top route is now invisible
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getOpacity(topRoute, tester), 0.0);
+ // Bottom route is still invisible, but moving towards the right.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getOpacity(bottomRoute, tester),
+ moreOrLessEquals(0, epsilon: 0.005));
+ double? bottomOffset = _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ );
+ expect(bottomOffset, lessThan(0.0));
+ expect(bottomOffset, greaterThan(-30.0));
+
+ // Jump to the middle of fading in
+ await tester.pump(const Duration(milliseconds: 90));
+ // Top route is still invisible
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getOpacity(topRoute, tester), 0.0);
+ // Bottom route is fading in
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getOpacity(bottomRoute, tester), greaterThan(0));
+ expect(_getOpacity(bottomRoute, tester), lessThan(1.0));
+ bottomOffset = _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ );
+ expect(bottomOffset, lessThan(0.0));
+ expect(bottomOffset, greaterThan(-30.0));
+
+ // Jump to the end of the transition
+ await tester.pump(const Duration(milliseconds: 120));
+ // Top route is not visible and is offset to the right.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ 30.0,
+ );
+ expect(_getOpacity(topRoute, tester), 0.0);
+ // Bottom route is not offset and is visible.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ 0.0,
+ );
+ expect(_getOpacity(bottomRoute, tester), 1.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text(topRoute), findsNothing);
+ expect(find.text(bottomRoute), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'SharedAxisTransition does not jump when interrupted',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ transitionType: SharedAxisTransitionType.horizontal,
+ ),
+ );
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(find.text(topRoute), findsNothing);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+
+ // Jump to halfway point of transition.
+ await tester.pump(const Duration(milliseconds: 150));
+ // Bottom route is fully faded out.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ final double halfwayBottomOffset = _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ );
+ expect(halfwayBottomOffset, lessThan(0.0));
+ expect(halfwayBottomOffset, greaterThan(-30.0));
+
+ // Top route is fading/coming in.
+ expect(find.text(topRoute), findsOneWidget);
+ final double halfwayTopOffset = _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ );
+ final double halfwayTopOpacity = _getOpacity(topRoute, tester);
+ expect(halfwayTopOffset, greaterThan(0.0));
+ expect(halfwayTopOffset, lessThan(30.0));
+ expect(halfwayTopOpacity, greaterThan(0.0));
+ expect(halfwayTopOpacity, lessThan(1.0));
+
+ // Interrupt the transition with a pop.
+ navigator.currentState!.pop();
+ await tester.pump();
+
+ // Nothing should change.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ halfwayBottomOffset,
+ );
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ expect(find.text(topRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ halfwayTopOffset,
+ );
+ expect(_getOpacity(topRoute, tester), halfwayTopOpacity);
+
+ // Jump to the 1/4 (75 ms) point of transition
+ await tester.pump(const Duration(milliseconds: 75));
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ lessThan(0.0),
+ );
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ greaterThan(-30.0),
+ );
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ greaterThan(halfwayBottomOffset),
+ );
+ expect(_getOpacity(bottomRoute, tester), greaterThan(0.0));
+ expect(_getOpacity(bottomRoute, tester), lessThan(1.0));
+
+ // Jump to the end.
+ await tester.pump(const Duration(milliseconds: 75));
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ 0.0,
+ );
+ expect(_getOpacity(bottomRoute, tester), 1.0);
+ expect(find.text(topRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.horizontal,
+ ),
+ 30.0,
+ );
+ expect(_getOpacity(topRoute, tester), 0.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text(topRoute), findsNothing);
+ expect(find.text(bottomRoute), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'State is not lost when transitioning',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ contentBuilder: (RouteSettings settings) {
+ return _StatefulTestWidget(
+ key: ValueKey<String?>(settings.name),
+ name: settings.name!,
+ );
+ },
+ transitionType: SharedAxisTransitionType.horizontal,
+ ),
+ );
+
+ final _StatefulTestWidgetState bottomState = tester.state(
+ find.byKey(const ValueKey<String?>(bottomRoute)),
+ );
+ expect(bottomState.widget.name, bottomRoute);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+ await tester.pump();
+
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ final _StatefulTestWidgetState topState = tester.state(
+ find.byKey(const ValueKey<String?>(topRoute)),
+ );
+ expect(topState.widget.name, topRoute);
+
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+ topState,
+ );
+
+ await tester.pumpAndSettle();
+ expect(
+ tester.state(find.byKey(
+ const ValueKey<String?>(bottomRoute),
+ skipOffstage: false,
+ )),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+ topState,
+ );
+
+ navigator.currentState!.pop();
+ await tester.pump();
+
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+ topState,
+ );
+
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+ topState,
+ );
+
+ await tester.pumpAndSettle();
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ expect(find.byKey(const ValueKey<String?>(topRoute)), findsNothing);
+ },
+ );
+
+ testWidgets('default fill color', (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ // The default fill color should be derived from ThemeData.canvasColor.
+ final Color defaultFillColor = ThemeData().canvasColor;
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ transitionType: SharedAxisTransitionType.horizontal,
+ ),
+ );
+
+ expect(find.text(bottomRoute), findsOneWidget);
+ Finder fillContainerFinder = find
+ .ancestor(
+ matching: find.byType(Container),
+ of: find.byKey(const ValueKey<String?>('/')),
+ )
+ .last;
+ expect(fillContainerFinder, findsOneWidget);
+ expect(tester.widget<Container>(fillContainerFinder).color,
+ defaultFillColor);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+ await tester.pumpAndSettle();
+
+ fillContainerFinder = find
+ .ancestor(
+ matching: find.byType(Container),
+ of: find.byKey(const ValueKey<String?>('/a')),
+ )
+ .last;
+ expect(fillContainerFinder, findsOneWidget);
+ expect(tester.widget<Container>(fillContainerFinder).color,
+ defaultFillColor);
+ });
+
+ testWidgets('custom fill color', (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ fillColor: Colors.green,
+ transitionType: SharedAxisTransitionType.horizontal,
+ ),
+ );
+
+ expect(find.text(bottomRoute), findsOneWidget);
+ Finder fillContainerFinder = find
+ .ancestor(
+ matching: find.byType(Container),
+ of: find.byKey(const ValueKey<String?>('/')),
+ )
+ .last;
+ expect(fillContainerFinder, findsOneWidget);
+ expect(tester.widget<Container>(fillContainerFinder).color, Colors.green);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+ await tester.pumpAndSettle();
+
+ fillContainerFinder = find
+ .ancestor(
+ matching: find.byType(Container),
+ of: find.byKey(const ValueKey<String?>('/a')),
+ )
+ .last;
+ expect(fillContainerFinder, findsOneWidget);
+ expect(tester.widget<Container>(fillContainerFinder).color, Colors.green);
+ });
+
+ testWidgets('should keep state', (WidgetTester tester) async {
+ final AnimationController animation = AnimationController(
+ vsync: const TestVSync(),
+ duration: const Duration(milliseconds: 300),
+ );
+ final AnimationController secondaryAnimation = AnimationController(
+ vsync: const TestVSync(),
+ duration: const Duration(milliseconds: 300),
+ );
+ await tester.pumpWidget(Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: SharedAxisTransition(
+ transitionType: SharedAxisTransitionType.horizontal,
+ child: const _StatefulTestWidget(name: 'Foo'),
+ animation: animation,
+ secondaryAnimation: secondaryAnimation,
+ ),
+ ),
+ ));
+ final State<StatefulWidget> state = tester.state(
+ find.byType(_StatefulTestWidget),
+ );
+ expect(state, isNotNull);
+
+ animation.forward();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ secondaryAnimation.forward();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ secondaryAnimation.reverse();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ animation.reverse();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ });
+ });
+
+ group('SharedAxisTransitionType.vertical', () {
+ testWidgets(
+ 'SharedAxisPageTransitionsBuilder builds a SharedAxisTransition',
+ (WidgetTester tester) async {
+ final AnimationController animation = AnimationController(
+ vsync: const TestVSync(),
+ );
+ final AnimationController secondaryAnimation = AnimationController(
+ vsync: const TestVSync(),
+ );
+
+ await tester.pumpWidget(
+ const SharedAxisPageTransitionsBuilder(
+ transitionType: SharedAxisTransitionType.vertical,
+ ).buildTransitions<void>(
+ null,
+ null,
+ animation,
+ secondaryAnimation,
+ const Placeholder(),
+ ),
+ );
+
+ expect(find.byType(SharedAxisTransition), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'SharedAxisTransition runs forward',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ transitionType: SharedAxisTransitionType.vertical,
+ ),
+ );
+
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ 0.0,
+ );
+ expect(_getOpacity(bottomRoute, tester), 1.0);
+ expect(find.text(topRoute), findsNothing);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+ await tester.pump();
+
+ // Bottom route is not offset and fully visible.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ 0.0,
+ );
+ expect(_getOpacity(bottomRoute, tester), 1.0);
+ // Top route is offset down by 30.0 pixels
+ // and not visible yet.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ 30.0,
+ );
+ expect(_getOpacity(topRoute, tester), 0.0);
+
+ // Jump 3/10ths of the way through the transition, bottom route
+ // should be be completely faded out while the top route
+ // is also completely faded out.
+ // Transition time: 300ms, 3/10 * 300ms = 90ms
+ await tester.pump(const Duration(milliseconds: 90));
+
+ // Bottom route is now invisible
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ // Top route is still invisible, but moving up.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getOpacity(topRoute, tester), 0.0);
+ double? topOffset = _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ );
+ expect(topOffset, lessThan(30.0));
+ expect(topOffset, greaterThan(0.0));
+
+ // Jump to the middle of fading in
+ await tester.pump(const Duration(milliseconds: 90));
+ // Bottom route is still invisible
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ // Top route is fading in
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getOpacity(topRoute, tester), greaterThan(0));
+ expect(_getOpacity(topRoute, tester), lessThan(1.0));
+ topOffset = _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ );
+ expect(topOffset, greaterThan(0.0));
+ expect(topOffset, lessThan(30.0));
+
+ // Jump to the end of the transition
+ await tester.pump(const Duration(milliseconds: 120));
+ // Bottom route is not visible.
+ expect(find.text(bottomRoute), findsOneWidget);
+
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ -30.0,
+ );
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ // Top route has no offset and is visible.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ 0.0,
+ );
+ expect(_getOpacity(topRoute, tester), 1.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text(bottomRoute), findsNothing);
+ expect(find.text(topRoute), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'SharedAxisTransition runs in reverse',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ transitionType: SharedAxisTransitionType.vertical,
+ ),
+ );
+
+ navigator.currentState!.pushNamed('/a');
+ await tester.pumpAndSettle();
+
+ expect(find.text(topRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ 0.0,
+ );
+ expect(_getOpacity(topRoute, tester), 1.0);
+ expect(find.text(bottomRoute), findsNothing);
+
+ navigator.currentState!.pop();
+ await tester.pump();
+
+ // Top route is is not offset and fully visible.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ 0.0,
+ );
+ expect(_getOpacity(topRoute, tester), 1.0);
+ // Bottom route is offset up and is not visible yet.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ -30.0,
+ );
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+
+ // Jump 3/10ths of the way through the transition, bottom route
+ // should be be completely faded out while the top route
+ // is also completely faded out.
+ // Transition time: 300ms, 3/10 * 300ms = 90ms
+ await tester.pump(const Duration(milliseconds: 90));
+
+ // Top route is now invisible
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getOpacity(topRoute, tester), 0.0);
+ // Bottom route is still invisible, but moving down.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getOpacity(bottomRoute, tester),
+ moreOrLessEquals(0, epsilon: 0.005),
+ );
+ double? bottomOffset = _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ );
+ expect(bottomOffset, lessThan(0.0));
+ expect(bottomOffset, greaterThan(-30.0));
+
+ // Jump to the middle of fading in
+ await tester.pump(const Duration(milliseconds: 90));
+ // Top route is still invisible
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getOpacity(topRoute, tester), 0.0);
+ // Bottom route is fading in
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getOpacity(bottomRoute, tester), greaterThan(0));
+ expect(_getOpacity(bottomRoute, tester), lessThan(1.0));
+ bottomOffset = _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ );
+ expect(bottomOffset, lessThan(0.0));
+ expect(bottomOffset, greaterThan(-30.0));
+
+ // Jump to the end of the transition
+ await tester.pump(const Duration(milliseconds: 120));
+ // Top route is not visible and is offset down.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ 30.0,
+ );
+ expect(_getOpacity(topRoute, tester), 0.0);
+ // Bottom route is not offset and is visible.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ 0.0,
+ );
+ expect(_getOpacity(bottomRoute, tester), 1.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text(topRoute), findsNothing);
+ expect(find.text(bottomRoute), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'SharedAxisTransition does not jump when interrupted',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ transitionType: SharedAxisTransitionType.vertical,
+ ),
+ );
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(find.text(topRoute), findsNothing);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+
+ // Jump to halfway point of transition.
+ await tester.pump(const Duration(milliseconds: 150));
+ // Bottom route is fully faded out.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ final double halfwayBottomOffset = _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ );
+ expect(halfwayBottomOffset, lessThan(0.0));
+ expect(halfwayBottomOffset, greaterThan(-30.0));
+
+ // Top route is fading/coming in.
+ expect(find.text(topRoute), findsOneWidget);
+ final double halfwayTopOffset = _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ );
+ final double halfwayTopOpacity = _getOpacity(topRoute, tester);
+ expect(halfwayTopOffset, greaterThan(0.0));
+ expect(halfwayTopOffset, lessThan(30.0));
+ expect(halfwayTopOpacity, greaterThan(0.0));
+ expect(halfwayTopOpacity, lessThan(1.0));
+
+ // Interrupt the transition with a pop.
+ navigator.currentState!.pop();
+ await tester.pump();
+
+ // Nothing should change.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ halfwayBottomOffset,
+ );
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ expect(find.text(topRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ halfwayTopOffset,
+ );
+ expect(_getOpacity(topRoute, tester), halfwayTopOpacity);
+
+ // Jump to the 1/4 (75 ms) point of transition
+ await tester.pump(const Duration(milliseconds: 75));
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ lessThan(0.0),
+ );
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ greaterThan(-30.0),
+ );
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ greaterThan(halfwayBottomOffset),
+ );
+ expect(_getOpacity(bottomRoute, tester), greaterThan(0.0));
+ expect(_getOpacity(bottomRoute, tester), lessThan(1.0));
+
+ // Jump to the end.
+ await tester.pump(const Duration(milliseconds: 75));
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ bottomRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ 0.0,
+ );
+ expect(_getOpacity(bottomRoute, tester), 1.0);
+ expect(find.text(topRoute), findsOneWidget);
+ expect(
+ _getTranslationOffset(
+ topRoute,
+ tester,
+ SharedAxisTransitionType.vertical,
+ ),
+ 30.0,
+ );
+ expect(_getOpacity(topRoute, tester), 0.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text(topRoute), findsNothing);
+ expect(find.text(bottomRoute), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'State is not lost when transitioning',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ contentBuilder: (RouteSettings settings) {
+ return _StatefulTestWidget(
+ key: ValueKey<String?>(settings.name),
+ name: settings.name!,
+ );
+ },
+ transitionType: SharedAxisTransitionType.vertical,
+ ),
+ );
+
+ final _StatefulTestWidgetState bottomState = tester.state(
+ find.byKey(const ValueKey<String?>(bottomRoute)),
+ );
+ expect(bottomState.widget.name, bottomRoute);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+ await tester.pump();
+
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ final _StatefulTestWidgetState topState = tester.state(
+ find.byKey(const ValueKey<String?>(topRoute)),
+ );
+ expect(topState.widget.name, topRoute);
+
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+ topState,
+ );
+
+ await tester.pumpAndSettle();
+ expect(
+ tester.state(find.byKey(
+ const ValueKey<String?>(bottomRoute),
+ skipOffstage: false,
+ )),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+ topState,
+ );
+
+ navigator.currentState!.pop();
+ await tester.pump();
+
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+ topState,
+ );
+
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+ topState,
+ );
+
+ await tester.pumpAndSettle();
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ expect(find.byKey(const ValueKey<String?>(topRoute)), findsNothing);
+ },
+ );
+
+ testWidgets('default fill color', (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ // The default fill color should be derived from ThemeData.canvasColor.
+ final Color defaultFillColor = ThemeData().canvasColor;
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ transitionType: SharedAxisTransitionType.vertical,
+ ),
+ );
+
+ expect(find.text(bottomRoute), findsOneWidget);
+ Finder fillContainerFinder = find
+ .ancestor(
+ matching: find.byType(Container),
+ of: find.byKey(const ValueKey<String?>('/')),
+ )
+ .last;
+ expect(fillContainerFinder, findsOneWidget);
+ expect(tester.widget<Container>(fillContainerFinder).color,
+ defaultFillColor);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+ await tester.pumpAndSettle();
+
+ fillContainerFinder = find
+ .ancestor(
+ matching: find.byType(Container),
+ of: find.byKey(const ValueKey<String?>('/a')),
+ )
+ .last;
+ expect(fillContainerFinder, findsOneWidget);
+ expect(tester.widget<Container>(fillContainerFinder).color,
+ defaultFillColor);
+ });
+
+ testWidgets('custom fill color', (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ fillColor: Colors.green,
+ transitionType: SharedAxisTransitionType.vertical,
+ ),
+ );
+
+ expect(find.text(bottomRoute), findsOneWidget);
+ Finder fillContainerFinder = find
+ .ancestor(
+ matching: find.byType(Container),
+ of: find.byKey(const ValueKey<String?>('/')),
+ )
+ .last;
+ expect(fillContainerFinder, findsOneWidget);
+ expect(tester.widget<Container>(fillContainerFinder).color, Colors.green);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+ await tester.pumpAndSettle();
+
+ fillContainerFinder = find
+ .ancestor(
+ matching: find.byType(Container),
+ of: find.byKey(const ValueKey<String?>('/a')),
+ )
+ .last;
+ expect(fillContainerFinder, findsOneWidget);
+ expect(tester.widget<Container>(fillContainerFinder).color, Colors.green);
+ });
+
+ testWidgets('should keep state', (WidgetTester tester) async {
+ final AnimationController animation = AnimationController(
+ vsync: const TestVSync(),
+ duration: const Duration(milliseconds: 300),
+ );
+ final AnimationController secondaryAnimation = AnimationController(
+ vsync: const TestVSync(),
+ duration: const Duration(milliseconds: 300),
+ );
+ await tester.pumpWidget(Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: SharedAxisTransition(
+ transitionType: SharedAxisTransitionType.vertical,
+ child: const _StatefulTestWidget(name: 'Foo'),
+ animation: animation,
+ secondaryAnimation: secondaryAnimation,
+ ),
+ ),
+ ));
+ final State<StatefulWidget> state = tester.state(
+ find.byType(_StatefulTestWidget),
+ );
+ expect(state, isNotNull);
+
+ animation.forward();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ secondaryAnimation.forward();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ secondaryAnimation.reverse();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ animation.reverse();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ });
+ });
+
+ group('SharedAxisTransitionType.scaled', () {
+ testWidgets(
+ 'SharedAxisPageTransitionsBuilder builds a SharedAxisTransition',
+ (WidgetTester tester) async {
+ final AnimationController animation = AnimationController(
+ vsync: const TestVSync(),
+ );
+ final AnimationController secondaryAnimation = AnimationController(
+ vsync: const TestVSync(),
+ );
+
+ await tester.pumpWidget(
+ const SharedAxisPageTransitionsBuilder(
+ transitionType: SharedAxisTransitionType.scaled,
+ ).buildTransitions<void>(
+ null,
+ null,
+ animation,
+ secondaryAnimation,
+ const Placeholder(),
+ ),
+ );
+
+ expect(find.byType(SharedAxisTransition), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'SharedAxisTransition runs forward',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ transitionType: SharedAxisTransitionType.scaled,
+ ),
+ );
+
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.0);
+ expect(_getOpacity(bottomRoute, tester), 1.0);
+ expect(find.text(topRoute), findsNothing);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+ await tester.pump();
+
+ // Bottom route is full size and fully visible.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.0);
+ expect(_getOpacity(bottomRoute, tester), 1.0);
+ // Top route is at 80% of full size and not visible yet.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 0.8);
+ expect(_getOpacity(topRoute, tester), 0.0);
+
+ // Jump 3/10ths of the way through the transition, bottom route
+ // should be be completely faded out while the top route
+ // is also completely faded out.
+ // Transition time: 300ms, 3/10 * 300ms = 90ms
+ await tester.pump(const Duration(milliseconds: 90));
+
+ // Bottom route is now invisible
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ // Top route is still invisible, but scaling up.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getOpacity(topRoute, tester), 0.0);
+ double topScale = _getScale(topRoute, tester);
+ expect(topScale, greaterThan(0.8));
+ expect(topScale, lessThan(1.0));
+
+ // Jump to the middle of fading in
+ await tester.pump(const Duration(milliseconds: 90));
+ // Bottom route is still invisible
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ // Top route is fading in
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getOpacity(topRoute, tester), greaterThan(0));
+ expect(_getOpacity(topRoute, tester), lessThan(1.0));
+ topScale = _getScale(topRoute, tester);
+ expect(topScale, greaterThan(0.8));
+ expect(topScale, lessThan(1.0));
+
+ // Jump to the end of the transition
+ await tester.pump(const Duration(milliseconds: 120));
+ // Bottom route is not visible.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.1);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ // Top route fully scaled in and visible.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 1.0);
+ expect(_getOpacity(topRoute, tester), 1.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text(bottomRoute), findsNothing);
+ expect(find.text(topRoute), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'SharedAxisTransition runs in reverse',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ transitionType: SharedAxisTransitionType.scaled,
+ ),
+ );
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pumpAndSettle();
+
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 1.0);
+ expect(_getOpacity(topRoute, tester), 1.0);
+ expect(find.text(bottomRoute), findsNothing);
+
+ navigator.currentState!.pop();
+ await tester.pump();
+
+ // Top route is full size and fully visible.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 1.0);
+ expect(_getOpacity(topRoute, tester), 1.0);
+ // Bottom route is at 110% of full size and not visible yet.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.1);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+
+ // Jump 3/10ths of the way through the transition, bottom route
+ // should be be completely faded out while the top route
+ // is also completely faded out.
+ // Transition time: 300ms, 3/10 * 300ms = 90ms
+ await tester.pump(const Duration(milliseconds: 90));
+
+ // Bottom route is now invisible
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getOpacity(topRoute, tester), 0.0);
+ // Top route is still invisible, but scaling down.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(
+ _getOpacity(bottomRoute, tester),
+ moreOrLessEquals(0, epsilon: 0.005),
+ );
+ double bottomScale = _getScale(bottomRoute, tester);
+ expect(bottomScale, greaterThan(1.0));
+ expect(bottomScale, lessThan(1.1));
+
+ // Jump to the middle of fading in
+ await tester.pump(const Duration(milliseconds: 90));
+ // Top route is still invisible
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getOpacity(topRoute, tester), 0.0);
+ // Bottom route is fading in
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getOpacity(bottomRoute, tester), greaterThan(0));
+ expect(_getOpacity(bottomRoute, tester), lessThan(1.0));
+ bottomScale = _getScale(bottomRoute, tester);
+ expect(bottomScale, greaterThan(1.0));
+ expect(bottomScale, lessThan(1.1));
+
+ // Jump to the end of the transition
+ await tester.pump(const Duration(milliseconds: 120));
+ // Top route is not visible.
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 0.8);
+ expect(_getOpacity(topRoute, tester), 0.0);
+ // Bottom route fully scaled in and visible.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.0);
+ expect(_getOpacity(bottomRoute, tester), 1.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text(topRoute), findsNothing);
+ expect(find.text(bottomRoute), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'SharedAxisTransition does not jump when interrupted',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ transitionType: SharedAxisTransitionType.scaled,
+ ),
+ );
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(find.text(topRoute), findsNothing);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+
+ // Jump to halfway point of transition.
+ await tester.pump(const Duration(milliseconds: 150));
+ // Bottom route is fully faded out.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ final double halfwayBottomScale = _getScale(bottomRoute, tester);
+ expect(halfwayBottomScale, greaterThan(1.0));
+ expect(halfwayBottomScale, lessThan(1.1));
+
+ // Top route is fading/scaling in.
+ expect(find.text(topRoute), findsOneWidget);
+ final double halfwayTopScale = _getScale(topRoute, tester);
+ final double halfwayTopOpacity = _getOpacity(topRoute, tester);
+ expect(halfwayTopScale, greaterThan(0.8));
+ expect(halfwayTopScale, lessThan(1.0));
+ expect(halfwayTopOpacity, greaterThan(0.0));
+ expect(halfwayTopOpacity, lessThan(1.0));
+
+ // Interrupt the transition with a pop.
+ navigator.currentState!.pop();
+ await tester.pump();
+
+ // Nothing should change.
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), halfwayBottomScale);
+ expect(_getOpacity(bottomRoute, tester), 0.0);
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), halfwayTopScale);
+ expect(_getOpacity(topRoute, tester), halfwayTopOpacity);
+
+ // Jump to the 1/4 (75 ms) point of transition
+ await tester.pump(const Duration(milliseconds: 75));
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), greaterThan(1.0));
+ expect(_getScale(bottomRoute, tester), lessThan(1.1));
+ expect(_getScale(bottomRoute, tester), lessThan(halfwayBottomScale));
+ expect(_getOpacity(bottomRoute, tester), greaterThan(0.0));
+ expect(_getOpacity(bottomRoute, tester), lessThan(1.0));
+
+ // Jump to the end.
+ await tester.pump(const Duration(milliseconds: 75));
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(_getScale(bottomRoute, tester), 1.0);
+ expect(_getOpacity(bottomRoute, tester), 1.0);
+ expect(find.text(topRoute), findsOneWidget);
+ expect(_getScale(topRoute, tester), 0.80);
+ expect(_getOpacity(topRoute, tester), 0.0);
+
+ await tester.pump(const Duration(milliseconds: 1));
+ expect(find.text(topRoute), findsNothing);
+ expect(find.text(bottomRoute), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'SharedAxisTransition properly disposes animation',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ transitionType: SharedAxisTransitionType.scaled,
+ ),
+ );
+ expect(find.text(bottomRoute), findsOneWidget);
+ expect(find.text(topRoute), findsNothing);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+
+ // Jump to halfway point of transition.
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(find.byType(SharedAxisTransition), findsNWidgets(2));
+
+ // Rebuild the app without the transition.
+ await tester.pumpWidget(
+ MaterialApp(
+ navigatorKey: navigator,
+ home: const Material(
+ child: Text('abc'),
+ ),
+ ),
+ );
+ await tester.pump();
+ // Transitions should have been disposed of.
+ expect(find.byType(SharedAxisTransition), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'State is not lost when transitioning',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ transitionType: SharedAxisTransitionType.scaled,
+ contentBuilder: (RouteSettings settings) {
+ return _StatefulTestWidget(
+ key: ValueKey<String?>(settings.name),
+ name: settings.name!,
+ );
+ },
+ ),
+ );
+
+ final _StatefulTestWidgetState bottomState = tester.state(
+ find.byKey(const ValueKey<String?>(bottomRoute)),
+ );
+ expect(bottomState.widget.name, bottomRoute);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+ await tester.pump();
+
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ final _StatefulTestWidgetState topState = tester.state(
+ find.byKey(const ValueKey<String?>(topRoute)),
+ );
+ expect(topState.widget.name, topRoute);
+
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+ topState,
+ );
+
+ await tester.pumpAndSettle();
+ expect(
+ tester.state(find.byKey(
+ const ValueKey<String?>(bottomRoute),
+ skipOffstage: false,
+ )),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+ topState,
+ );
+
+ navigator.currentState!.pop();
+ await tester.pump();
+
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+ topState,
+ );
+
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+ topState,
+ );
+
+ await tester.pumpAndSettle();
+ expect(
+ tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+ bottomState,
+ );
+ expect(find.byKey(const ValueKey<String?>(topRoute)), findsNothing);
+ },
+ );
+
+ testWidgets('default fill color', (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ // The default fill color should be derived from ThemeData.canvasColor.
+ final Color defaultFillColor = ThemeData().canvasColor;
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ transitionType: SharedAxisTransitionType.scaled,
+ ),
+ );
+
+ expect(find.text(bottomRoute), findsOneWidget);
+ Finder fillContainerFinder = find
+ .ancestor(
+ matching: find.byType(Container),
+ of: find.byKey(const ValueKey<String?>('/')),
+ )
+ .last;
+ expect(fillContainerFinder, findsOneWidget);
+ expect(tester.widget<Container>(fillContainerFinder).color,
+ defaultFillColor);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+ await tester.pumpAndSettle();
+
+ fillContainerFinder = find
+ .ancestor(
+ matching: find.byType(Container),
+ of: find.byKey(const ValueKey<String?>('/a')),
+ )
+ .last;
+ expect(fillContainerFinder, findsOneWidget);
+ expect(tester.widget<Container>(fillContainerFinder).color,
+ defaultFillColor);
+ });
+
+ testWidgets('custom fill color', (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+ const String bottomRoute = '/';
+ const String topRoute = '/a';
+
+ await tester.pumpWidget(
+ _TestWidget(
+ navigatorKey: navigator,
+ fillColor: Colors.green,
+ transitionType: SharedAxisTransitionType.scaled,
+ ),
+ );
+
+ expect(find.text(bottomRoute), findsOneWidget);
+ Finder fillContainerFinder = find
+ .ancestor(
+ matching: find.byType(Container),
+ of: find.byKey(const ValueKey<String?>('/')),
+ )
+ .last;
+ expect(fillContainerFinder, findsOneWidget);
+ expect(tester.widget<Container>(fillContainerFinder).color, Colors.green);
+
+ navigator.currentState!.pushNamed(topRoute);
+ await tester.pump();
+ await tester.pumpAndSettle();
+
+ fillContainerFinder = find
+ .ancestor(
+ matching: find.byType(Container),
+ of: find.byKey(const ValueKey<String?>('/a')),
+ )
+ .last;
+ expect(fillContainerFinder, findsOneWidget);
+ expect(tester.widget<Container>(fillContainerFinder).color, Colors.green);
+ });
+
+ testWidgets('should keep state', (WidgetTester tester) async {
+ final AnimationController animation = AnimationController(
+ vsync: const TestVSync(),
+ duration: const Duration(milliseconds: 300),
+ );
+ final AnimationController secondaryAnimation = AnimationController(
+ vsync: const TestVSync(),
+ duration: const Duration(milliseconds: 300),
+ );
+ await tester.pumpWidget(Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: SharedAxisTransition(
+ transitionType: SharedAxisTransitionType.scaled,
+ child: const _StatefulTestWidget(name: 'Foo'),
+ animation: animation,
+ secondaryAnimation: secondaryAnimation,
+ ),
+ ),
+ ));
+ final State<StatefulWidget> state = tester.state(
+ find.byType(_StatefulTestWidget),
+ );
+ expect(state, isNotNull);
+
+ animation.forward();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ secondaryAnimation.forward();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ secondaryAnimation.reverse();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+ animation.reverse();
+ await tester.pump();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pump(const Duration(milliseconds: 150));
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ await tester.pumpAndSettle();
+ expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+ });
+ });
+}
+
+double _getOpacity(String key, WidgetTester tester) {
+ final Finder finder = find.ancestor(
+ of: find.byKey(ValueKey<String?>(key)),
+ matching: find.byType(FadeTransition),
+ );
+ return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
+ final FadeTransition transition = widget as FadeTransition;
+ return a * transition.opacity.value;
+ });
+}
+
+double _getTranslationOffset(
+ String key,
+ WidgetTester tester,
+ SharedAxisTransitionType transitionType,
+) {
+ final Finder finder = find.ancestor(
+ of: find.byKey(ValueKey<String?>(key)),
+ matching: find.byType(Transform),
+ );
+
+ switch (transitionType) {
+ case SharedAxisTransitionType.horizontal:
+ return tester.widgetList<Transform>(finder).fold<double>(0.0,
+ (double a, Widget widget) {
+ final Transform transition = widget as Transform;
+ final Vector3 translation = transition.transform.getTranslation();
+ return a + translation.x;
+ });
+ case SharedAxisTransitionType.vertical:
+ return tester.widgetList<Transform>(finder).fold<double>(0.0,
+ (double a, Widget widget) {
+ final Transform transition = widget as Transform;
+ final Vector3 translation = transition.transform.getTranslation();
+ return a + translation.y;
+ });
+ case SharedAxisTransitionType.scaled:
+ assert(
+ false,
+ 'SharedAxisTransitionType.scaled does not have a translation offset',
+ );
+ return 0.0;
+ }
+}
+
+double _getScale(String key, WidgetTester tester) {
+ final Finder finder = find.ancestor(
+ of: find.byKey(ValueKey<String?>(key)),
+ matching: find.byType(ScaleTransition),
+ );
+ return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
+ final ScaleTransition transition = widget as ScaleTransition;
+ return a * transition.scale.value;
+ });
+}
+
+class _TestWidget extends StatelessWidget {
+ const _TestWidget({
+ required this.navigatorKey,
+ this.contentBuilder,
+ required this.transitionType,
+ this.fillColor,
+ });
+
+ final Key navigatorKey;
+ final _ContentBuilder? contentBuilder;
+ final SharedAxisTransitionType transitionType;
+ final Color? fillColor;
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ navigatorKey: navigatorKey as GlobalKey<NavigatorState>?,
+ theme: ThemeData(
+ platform: TargetPlatform.android,
+ pageTransitionsTheme: PageTransitionsTheme(
+ builders: <TargetPlatform, PageTransitionsBuilder>{
+ TargetPlatform.android: SharedAxisPageTransitionsBuilder(
+ fillColor: fillColor,
+ transitionType: transitionType,
+ ),
+ },
+ ),
+ ),
+ onGenerateRoute: (RouteSettings settings) {
+ return MaterialPageRoute<void>(
+ settings: settings,
+ builder: (BuildContext context) {
+ return contentBuilder != null
+ ? contentBuilder!(settings)
+ : Center(
+ key: ValueKey<String?>(settings.name),
+ child: Text(settings.name!),
+ );
+ },
+ );
+ },
+ );
+ }
+}
+
+class _StatefulTestWidget extends StatefulWidget {
+ const _StatefulTestWidget({Key? key, required this.name}) : super(key: key);
+
+ final String name;
+
+ @override
+ State<_StatefulTestWidget> createState() => _StatefulTestWidgetState();
+}
+
+class _StatefulTestWidgetState extends State<_StatefulTestWidget> {
+ @override
+ Widget build(BuildContext context) {
+ return Text(widget.name);
+ }
+}
+
+typedef _ContentBuilder = Widget Function(RouteSettings settings);
diff --git a/packages/cross_file/CHANGELOG.md b/packages/cross_file/CHANGELOG.md
new file mode 100644
index 0000000..960c870
--- /dev/null
+++ b/packages/cross_file/CHANGELOG.md
@@ -0,0 +1,34 @@
+## 0.3.1+1
+
+* Rehomed to `flutter/packages` repository.
+
+## 0.3.1
+
+* Fix nullability of `XFileBase`'s `path` and `name` to match the
+ implementations to avoid potential analyzer issues.
+
+## 0.3.0
+
+* Migrated package to null-safety.
+* **breaking change** According to our unit tests, the API should be backwards-compatible. Some relevant changes were made, however:
+ * Web: `lastModified` returns the epoch time as a default value, to maintain the `Future<DateTime>` return type (and not `null`)
+
+## 0.2.1
+
+* Prepare for breaking `package:http` change.
+
+## 0.2.0
+
+* **breaking change** Make sure the `saveTo` method returns a `Future` so it can be awaited and users are sure the file has been written to disk.
+
+## 0.1.0+2
+
+* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276))
+
+## 0.1.0+1
+
+* Update Flutter SDK constraint.
+
+## 0.1.0
+
+* Initial open-source release.
diff --git a/packages/cross_file/LICENSE b/packages/cross_file/LICENSE
new file mode 100644
index 0000000..2c91f14
--- /dev/null
+++ b/packages/cross_file/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2020 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google 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
diff --git a/packages/cross_file/README.md b/packages/cross_file/README.md
new file mode 100644
index 0000000..65bd418
--- /dev/null
+++ b/packages/cross_file/README.md
@@ -0,0 +1,34 @@
+# cross_file
+
+An abstraction to allow working with files across multiple platforms.
+
+# Usage
+
+Import `package:cross/cross_info.dart`, instantiate a `CrossFile`
+using a path or byte array and use its methods and properties to
+access the file and its metadata.
+
+Example:
+
+```dart
+import 'package:cross_file/cross_file.dart';
+
+final file = CrossFile('assets/hello.txt');
+
+print('File information:');
+print('- Path: ${file.path}');
+print('- Name: ${file.name}');
+print('- MIME type: ${file.mimeType}');
+
+final fileContent = await file.readAsString();
+print('Content of the file: ${fileContent}'); // e.g. "Moto G (4)"
+```
+
+You will find links to the API docs on the [pub page](https://pub.dev/packages/cross_file).
+
+## 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).
\ No newline at end of file
diff --git a/packages/cross_file/lib/cross_file.dart b/packages/cross_file/lib/cross_file.dart
new file mode 100644
index 0000000..a3e2873
--- /dev/null
+++ b/packages/cross_file/lib/cross_file.dart
@@ -0,0 +1,5 @@
+// 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.
+
+export 'src/x_file.dart';
diff --git a/packages/cross_file/lib/src/types/base.dart b/packages/cross_file/lib/src/types/base.dart
new file mode 100644
index 0000000..98c2f8c
--- /dev/null
+++ b/packages/cross_file/lib/src/types/base.dart
@@ -0,0 +1,87 @@
+// 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:convert';
+import 'dart:typed_data';
+
+/// The interface for a CrossFile.
+///
+/// A CrossFile is a container that wraps the path of a selected
+/// file by the user and (in some platforms, like web) the bytes
+/// with the contents of the file.
+///
+/// This class is a very limited subset of dart:io [File], so all
+/// the methods should seem familiar.
+abstract class XFileBase {
+ /// Construct a CrossFile
+ // ignore: avoid_unused_constructor_parameters
+ XFileBase(String? path);
+
+ /// Save the CrossFile at the indicated file path.
+ Future<void> saveTo(String path) {
+ throw UnimplementedError('saveTo has not been implemented.');
+ }
+
+ /// Get the path of the picked file.
+ ///
+ /// This should only be used as a backwards-compatibility clutch
+ /// for mobile apps, or cosmetic reasons only (to show the user
+ /// the path they've picked).
+ ///
+ /// Accessing the data contained in the picked file by its path
+ /// is platform-dependant (and won't work on web), so use the
+ /// byte getters in the CrossFile instance instead.
+ String get path {
+ throw UnimplementedError('.path has not been implemented.');
+ }
+
+ /// The name of the file as it was selected by the user in their device.
+ ///
+ /// Use only for cosmetic reasons, do not try to use this as a path.
+ String get name {
+ throw UnimplementedError('.name has not been implemented.');
+ }
+
+ /// For web, it may be necessary for a file to know its MIME type.
+ String? get mimeType {
+ throw UnimplementedError('.mimeType has not been implemented.');
+ }
+
+ /// Get the length of the file. Returns a `Future<int>` that completes with the length in bytes.
+ Future<int> length() {
+ throw UnimplementedError('.length() has not been implemented.');
+ }
+
+ /// Synchronously read the entire file contents as a string using the given [Encoding].
+ ///
+ /// By default, `encoding` is [utf8].
+ ///
+ /// Throws Exception if the operation fails.
+ Future<String> 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<Uint8List> 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<Uint8List> openRead([int? start, int? end]) {
+ throw UnimplementedError('openRead() has not been implemented.');
+ }
+
+ /// Get the last-modified time for the CrossFile
+ Future<DateTime> lastModified() {
+ throw UnimplementedError('openRead() has not been implemented.');
+ }
+}
diff --git a/packages/cross_file/lib/src/types/html.dart b/packages/cross_file/lib/src/types/html.dart
new file mode 100644
index 0000000..ef69af5
--- /dev/null
+++ b/packages/cross_file/lib/src/types/html.dart
@@ -0,0 +1,143 @@
+// 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:convert';
+import 'dart:html';
+import 'dart:typed_data';
+
+import 'package:meta/meta.dart';
+
+import '../web_helpers/web_helpers.dart';
+import './base.dart';
+
+/// A CrossFile that works on web.
+///
+/// It wraps the bytes of a selected file.
+class XFile extends XFileBase {
+ /// Construct a CrossFile object from its ObjectUrl.
+ ///
+ /// Optionally, this can be initialized with `bytes` and `length`
+ /// so no http requests are performed to retrieve files later.
+ ///
+ /// `name` needs to be passed from the outside, since we only have
+ /// access to it while we create the ObjectUrl.
+ XFile(
+ this.path, {
+ this.mimeType,
+ String? name,
+ int? length,
+ Uint8List? bytes,
+ DateTime? lastModified,
+ @visibleForTesting CrossFileTestOverrides? overrides,
+ }) : _data = bytes,
+ _length = length,
+ _overrides = overrides,
+ _lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0),
+ name = name ?? '',
+ super(path);
+
+ /// Construct an CrossFile from its data
+ XFile.fromData(
+ Uint8List bytes, {
+ this.mimeType,
+ String? name,
+ int? length,
+ DateTime? lastModified,
+ String? path,
+ @visibleForTesting CrossFileTestOverrides? overrides,
+ }) : _data = bytes,
+ _length = length,
+ _overrides = overrides,
+ _lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0),
+ name = name ?? '',
+ super(path) {
+ if (path == null) {
+ final Blob blob = (mimeType == null)
+ ? Blob(<dynamic>[bytes])
+ : Blob(<dynamic>[bytes], mimeType);
+ this.path = Url.createObjectUrl(blob);
+ } else {
+ this.path = path;
+ }
+ }
+
+ @override
+ final String? mimeType;
+ @override
+ final String name;
+ @override
+ late String path;
+
+ final Uint8List? _data;
+ final int? _length;
+ final DateTime? _lastModified;
+
+ late Element _target;
+
+ final CrossFileTestOverrides? _overrides;
+
+ bool get _hasTestOverrides => _overrides != null;
+
+ @override
+ Future<DateTime> lastModified() async =>
+ Future<DateTime>.value(_lastModified);
+
+ Future<Uint8List> get _bytes async {
+ if (_data != null) {
+ return Future<Uint8List>.value(UnmodifiableUint8ListView(_data!));
+ }
+
+ // We can force 'response' to be a byte buffer by passing responseType:
+ final ByteBuffer? response =
+ (await HttpRequest.request(path, responseType: 'arraybuffer')).response;
+
+ return response?.asUint8List() ?? Uint8List(0);
+ }
+
+ @override
+ Future<int> length() async => _length ?? (await _bytes).length;
+
+ @override
+ Future<String> readAsString({Encoding encoding = utf8}) async {
+ return encoding.decode(await _bytes);
+ }
+
+ @override
+ Future<Uint8List> readAsBytes() async =>
+ Future<Uint8List>.value(await _bytes);
+
+ @override
+ Stream<Uint8List> openRead([int? start, int? end]) async* {
+ final Uint8List bytes = await _bytes;
+ yield bytes.sublist(start ?? 0, end ?? bytes.length);
+ }
+
+ /// Saves the data of this CrossFile at the location indicated by path.
+ /// For the web implementation, the path variable is ignored.
+ @override
+ Future<void> saveTo(String path) async {
+ // Create a DOM container where we can host the anchor.
+ _target = ensureInitialized('__x_file_dom_element');
+
+ // Create an <a> tag with the appropriate download attributes and click it
+ // May be overridden with CrossFileTestOverrides
+ final AnchorElement element = _hasTestOverrides
+ ? _overrides!.createAnchorElement(this.path, name) as AnchorElement
+ : createAnchorElement(this.path, name);
+
+ // Clear the children in our container so we can add an element to click
+ _target.children.clear();
+ addElementToContainerAndClick(_target, element);
+ }
+}
+
+/// Overrides some functions to allow testing
+@visibleForTesting
+class CrossFileTestOverrides {
+ /// Default constructor for overrides
+ CrossFileTestOverrides({required this.createAnchorElement});
+
+ /// For overriding the creation of the file input element.
+ Element Function(String href, String suggestedName) createAnchorElement;
+}
diff --git a/packages/cross_file/lib/src/types/interface.dart b/packages/cross_file/lib/src/types/interface.dart
new file mode 100644
index 0000000..91afac5
--- /dev/null
+++ b/packages/cross_file/lib/src/types/interface.dart
@@ -0,0 +1,60 @@
+// 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:typed_data';
+import 'package:meta/meta.dart';
+
+import './base.dart';
+
+// ignore_for_file: avoid_unused_constructor_parameters
+
+/// A CrossFile is a cross-platform, simplified File abstraction.
+///
+/// It wraps the bytes of a selected file, and its (platform-dependant) path.
+class XFile extends XFileBase {
+ /// Construct a CrossFile object from its path.
+ ///
+ /// Optionally, this can be initialized with `bytes` and `length`
+ /// so no http requests are performed to retrieve data later.
+ ///
+ /// `name` may be passed from the outside, for those cases where the effective
+ /// `path` of the file doesn't match what the user sees when selecting it
+ /// (like in web)
+ XFile(
+ String path, {
+ String? mimeType,
+ String? name,
+ int? length,
+ Uint8List? bytes,
+ DateTime? lastModified,
+ @visibleForTesting CrossFileTestOverrides? overrides,
+ }) : super(path) {
+ throw UnimplementedError(
+ 'CrossFile is not available in your current platform.');
+ }
+
+ /// Construct a CrossFile object from its data
+ XFile.fromData(
+ Uint8List bytes, {
+ String? mimeType,
+ String? name,
+ int? length,
+ DateTime? lastModified,
+ String? path,
+ @visibleForTesting CrossFileTestOverrides? overrides,
+ }) : super(path) {
+ throw UnimplementedError(
+ 'CrossFile is not available in your current platform.');
+ }
+}
+
+/// Overrides some functions of CrossFile for testing purposes
+@visibleForTesting
+class CrossFileTestOverrides {
+ /// Default constructor for overrides
+ CrossFileTestOverrides({required this.createAnchorElement});
+
+ /// For overriding the creation of the file input element.
+ dynamic Function(String href, String suggestedName) createAnchorElement;
+}
diff --git a/packages/cross_file/lib/src/types/io.dart b/packages/cross_file/lib/src/types/io.dart
new file mode 100644
index 0000000..6d649ce
--- /dev/null
+++ b/packages/cross_file/lib/src/types/io.dart
@@ -0,0 +1,119 @@
+// 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:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import './base.dart';
+
+// ignore_for_file: avoid_unused_constructor_parameters
+
+/// A CrossFile backed by a dart:io File.
+class XFile extends XFileBase {
+ /// Construct a CrossFile object backed by a dart:io File.
+ XFile(
+ String path, {
+ this.mimeType,
+ String? name,
+ int? length,
+ Uint8List? bytes,
+ DateTime? lastModified,
+ }) : _file = File(path),
+ _bytes = null,
+ _lastModified = lastModified,
+ super(path);
+
+ /// Construct an CrossFile from its data
+ XFile.fromData(
+ Uint8List bytes, {
+ this.mimeType,
+ String? path,
+ String? name,
+ int? length,
+ DateTime? lastModified,
+ }) : _bytes = bytes,
+ _file = File(path ?? ''),
+ _length = length,
+ _lastModified = lastModified,
+ super(path) {
+ if (length == null) {
+ _length = bytes.length;
+ }
+ }
+
+ final File _file;
+ @override
+ final String? mimeType;
+ final DateTime? _lastModified;
+ int? _length;
+
+ final Uint8List? _bytes;
+
+ @override
+ Future<DateTime> lastModified() {
+ if (_lastModified != null) {
+ return Future<DateTime>.value(_lastModified);
+ }
+ // ignore: avoid_slow_async_io
+ return _file.lastModified();
+ }
+
+ @override
+ Future<void> saveTo(String path) async {
+ final File fileToSave = File(path);
+ await fileToSave.writeAsBytes(_bytes ?? (await readAsBytes()));
+ await fileToSave.create();
+ }
+
+ @override
+ String get path {
+ return _file.path;
+ }
+
+ @override
+ String get name {
+ return _file.path.split(Platform.pathSeparator).last;
+ }
+
+ @override
+ Future<int> length() {
+ if (_length != null) {
+ return Future<int>.value(_length);
+ }
+ return _file.length();
+ }
+
+ @override
+ Future<String> readAsString({Encoding encoding = utf8}) {
+ if (_bytes != null) {
+ return Future<String>.value(String.fromCharCodes(_bytes!));
+ }
+ return _file.readAsString(encoding: encoding);
+ }
+
+ @override
+ Future<Uint8List> readAsBytes() {
+ if (_bytes != null) {
+ return Future<Uint8List>.value(_bytes);
+ }
+ return _file.readAsBytes();
+ }
+
+ Stream<Uint8List> _getBytes(int? start, int? end) async* {
+ final Uint8List bytes = _bytes!;
+ yield bytes.sublist(start ?? 0, end ?? bytes.length);
+ }
+
+ @override
+ Stream<Uint8List> openRead([int? start, int? end]) {
+ if (_bytes != null) {
+ return _getBytes(start, end);
+ } else {
+ return _file
+ .openRead(start ?? 0, end)
+ .map((List<int> chunk) => Uint8List.fromList(chunk));
+ }
+ }
+}
diff --git a/packages/cross_file/lib/src/web_helpers/web_helpers.dart b/packages/cross_file/lib/src/web_helpers/web_helpers.dart
new file mode 100644
index 0000000..9440d8a
--- /dev/null
+++ b/packages/cross_file/lib/src/web_helpers/web_helpers.dart
@@ -0,0 +1,38 @@
+// 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: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/cross_file/lib/src/x_file.dart b/packages/cross_file/lib/src/x_file.dart
new file mode 100644
index 0000000..6136bff
--- /dev/null
+++ b/packages/cross_file/lib/src/x_file.dart
@@ -0,0 +1,7 @@
+// 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.
+
+export 'types/interface.dart'
+ if (dart.library.html) 'types/html.dart'
+ if (dart.library.io) 'types/io.dart';
diff --git a/packages/cross_file/pubspec.yaml b/packages/cross_file/pubspec.yaml
new file mode 100644
index 0000000..5195602
--- /dev/null
+++ b/packages/cross_file/pubspec.yaml
@@ -0,0 +1,18 @@
+name: cross_file
+description: An abstraction to allow working with files across multiple platforms.
+repository: https://github.com/flutter/packages/tree/master/packages/cross_file
+version: 0.3.1+1
+
+dependencies:
+ flutter:
+ sdk: flutter
+ meta: ^1.3.0
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+ pedantic: ^1.10.0
+
+environment:
+ sdk: ">=2.12.0 <3.0.0"
+ flutter: ">=1.22.0"
diff --git a/packages/cross_file/test/assets/hello.txt b/packages/cross_file/test/assets/hello.txt
new file mode 100644
index 0000000..5dd01c1
--- /dev/null
+++ b/packages/cross_file/test/assets/hello.txt
@@ -0,0 +1 @@
+Hello, world!
\ No newline at end of file
diff --git a/packages/cross_file/test/x_file_html_test.dart b/packages/cross_file/test/x_file_html_test.dart
new file mode 100644
index 0000000..aadcc20
--- /dev/null
+++ b/packages/cross_file/test/x_file_html_test.dart
@@ -0,0 +1,108 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@TestOn('chrome') // Uses web-only Flutter SDK
+
+import 'dart:convert';
+import 'dart:html' as html;
+import 'dart:typed_data';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:cross_file/cross_file.dart';
+
+const String expectedStringContents = 'Hello, world!';
+final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents));
+final html.File textFile = html.File(<Object>[bytes], 'hello.txt');
+final String textFileUrl = html.Url.createObjectUrl(textFile);
+
+void main() {
+ group('Create with an objectUrl', () {
+ final XFile file = XFile(textFileUrl);
+
+ test('Can be read as a string', () async {
+ expect(await file.readAsString(), equals(expectedStringContents));
+ });
+ test('Can be read as bytes', () async {
+ expect(await file.readAsBytes(), equals(bytes));
+ });
+
+ test('Can be read as a stream', () async {
+ expect(await file.openRead().first, equals(bytes));
+ });
+
+ test('Stream can be sliced', () async {
+ expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+ });
+ });
+
+ group('Create from data', () {
+ final XFile file = XFile.fromData(bytes);
+
+ test('Can be read as a string', () async {
+ expect(await file.readAsString(), equals(expectedStringContents));
+ });
+ test('Can be read as bytes', () async {
+ expect(await file.readAsBytes(), equals(bytes));
+ });
+
+ test('Can be read as a stream', () async {
+ expect(await file.openRead().first, equals(bytes));
+ });
+
+ test('Stream can be sliced', () async {
+ expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+ });
+ });
+
+ group('saveTo(..)', () {
+ const String crossFileDomElementId = '__x_file_dom_element';
+
+ group('CrossFile saveTo(..)', () {
+ test('creates a DOM container', () async {
+ final XFile file = XFile.fromData(bytes);
+
+ await file.saveTo('');
+
+ final html.Element? container =
+ html.querySelector('#$crossFileDomElementId');
+
+ expect(container, isNotNull);
+ });
+
+ test('create anchor element', () async {
+ final XFile file = XFile.fromData(bytes, name: textFile.name);
+
+ await file.saveTo('path');
+
+ final html.Element container =
+ html.querySelector('#$crossFileDomElementId')!;
+ final html.AnchorElement element = container.children
+ .firstWhere((html.Element element) => element.tagName == 'A')
+ as html.AnchorElement;
+
+ // if element is not found, the `firstWhere` call will throw StateError.
+ expect(element.href, file.path);
+ expect(element.download, file.name);
+ });
+
+ test('anchor element is clicked', () async {
+ final html.AnchorElement mockAnchor = html.AnchorElement();
+
+ final CrossFileTestOverrides overrides = CrossFileTestOverrides(
+ createAnchorElement: (_, __) => mockAnchor,
+ );
+
+ final XFile file =
+ XFile.fromData(bytes, name: textFile.name, overrides: overrides);
+
+ bool clicked = false;
+ mockAnchor.onClick.listen((html.MouseEvent event) => clicked = true);
+
+ await file.saveTo('path');
+
+ expect(clicked, true);
+ });
+ });
+ });
+}
diff --git a/packages/cross_file/test/x_file_io_test.dart b/packages/cross_file/test/x_file_io_test.dart
new file mode 100644
index 0000000..a8edbe5
--- /dev/null
+++ b/packages/cross_file/test/x_file_io_test.dart
@@ -0,0 +1,90 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@TestOn('vm') // Uses dart:io
+
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:cross_file/cross_file.dart';
+
+final String pathPrefix =
+ Directory.current.path.endsWith('test') ? './assets/' : './test/assets/';
+final String path = pathPrefix + 'hello.txt';
+const String expectedStringContents = 'Hello, world!';
+final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents));
+final File textFile = File(path);
+final String textFilePath = textFile.path;
+
+void main() {
+ group('Create with a path', () {
+ final XFile file = XFile(textFilePath);
+
+ test('Can be read as a string', () async {
+ expect(await file.readAsString(), equals(expectedStringContents));
+ });
+ test('Can be read as bytes', () async {
+ expect(await file.readAsBytes(), equals(bytes));
+ });
+
+ test('Can be read as a stream', () async {
+ expect(await file.openRead().first, equals(bytes));
+ });
+
+ test('Stream can be sliced', () async {
+ expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+ });
+
+ test('saveTo(..) creates file', () async {
+ final File removeBeforeTest = File(pathPrefix + 'newFilePath.txt');
+ if (removeBeforeTest.existsSync()) {
+ await removeBeforeTest.delete();
+ }
+
+ await file.saveTo(pathPrefix + 'newFilePath.txt');
+ final File newFile = File(pathPrefix + 'newFilePath.txt');
+
+ expect(newFile.existsSync(), isTrue);
+ expect(newFile.readAsStringSync(), 'Hello, world!');
+
+ await newFile.delete();
+ });
+ });
+
+ group('Create with data', () {
+ final XFile file = XFile.fromData(bytes);
+
+ test('Can be read as a string', () async {
+ expect(await file.readAsString(), equals(expectedStringContents));
+ });
+ test('Can be read as bytes', () async {
+ expect(await file.readAsBytes(), equals(bytes));
+ });
+
+ test('Can be read as a stream', () async {
+ expect(await file.openRead().first, equals(bytes));
+ });
+
+ test('Stream can be sliced', () async {
+ expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+ });
+
+ test('Function saveTo(..) creates file', () async {
+ final File removeBeforeTest = File(pathPrefix + 'newFileData.txt');
+ if (removeBeforeTest.existsSync()) {
+ await removeBeforeTest.delete();
+ }
+
+ await file.saveTo(pathPrefix + 'newFileData.txt');
+ final File newFile = File(pathPrefix + 'newFileData.txt');
+
+ expect(newFile.existsSync(), isTrue);
+ expect(newFile.readAsStringSync(), 'Hello, world!');
+
+ await newFile.delete();
+ });
+ });
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md b/packages/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md
new file mode 100644
index 0000000..e2ccf47
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md
@@ -0,0 +1,34 @@
+## 2.0.2
+
+* Update example to use a scope name as a constant from the People API, instead of a hardcoded string.
+* Remove `x` bit from README and pubspec.yaml
+
+## 2.0.1
+
+* Rehomed to `flutter/packages` repository.
+* Update to `googleapis_auth: ^1.1.0`.
+
+## 2.0.0
+
+* Migrate to null safety.
+* Fixes the requested scopes to use the `GoogleSignIn` instance's `scopes`.
+
+## 1.0.4
+
+* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets.
+
+## 1.0.3
+
+* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276))
+
+## 1.0.2
+
+* Update Flutter SDK constraint.
+
+## 1.0.1
+
+* Update android compileSdkVersion to 29.
+
+## 1.0.0
+
+* First published version.
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/LICENSE b/packages/extension_google_sign_in_as_googleapis_auth/LICENSE
new file mode 100644
index 0000000..b707cc8
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2020 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/README.md b/packages/extension_google_sign_in_as_googleapis_auth/README.md
new file mode 100644
index 0000000..c97b22f
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/README.md
@@ -0,0 +1,46 @@
+# extension_google_sign_in_as_googleapis_auth
+
+A bridge package between Flutter's [`google_sign_in` plugin](https://pub.dev/packages/google_sign_in) and Dart's [`googleapis` package](https://pub.dev/packages/googleapis), that is able to create [`googleapis_auth`-like `AuthClient` instances](https://pub.dev/documentation/googleapis_auth/latest/googleapis_auth.auth/AuthClient-class.html) directly from the `GoogleSignIn` plugin.
+
+## Usage
+
+This package is implemented as an [extension method](https://dart.dev/guides/language/extension-methods) on top of the `GoogleSignIn` plugin.
+
+In order to use it, you need to add a `dependency` to your `pubspec.yaml`. Then, wherever you're importing `package:google_sign_in/google_sign_in.dart`, add the following:
+
+```dart
+...
+import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart';
+...
+```
+
+From that moment on, your `GoogleSignIn` instance will have an additional `Future<AuthClient?> authenticatedClient()` method that you can call once your sign in is successful to retrieve an `AuthClient`.
+
+That object can then be used to create instances of `googleapis` API clients:
+
+```dart
+...
+final peopleApi = PeopleApi((await _googleSignIn.authenticatedClient())!);
+final response = await peopleApi.people.connections.list(
+ 'people/me',
+ personFields: 'names',
+);
+...
+```
+
+## Example
+
+This package contains a modified version of Flutter's Google Sign In example app that uses `package:googleapis`' API clients, instead of raw http requests.
+
+See it [here](https://github.com/flutter/packages/blob/master/packages/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart).
+
+The original code (and its license) can be seen [here](https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in/example/lib/main.dart).
+
+## Testing
+
+Run tests with `flutter test`.
+
+## Issues and feedback
+
+Please file [issues](https://github.com/flutter/flutter/issues/new)
+to send feedback or report a bug. Thank you!
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/README.md b/packages/extension_google_sign_in_as_googleapis_auth/example/README.md
new file mode 100644
index 0000000..a7ad235
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/README.md
@@ -0,0 +1,8 @@
+# extension_google_sign_in_example
+
+Demonstrates how to use the google_sign_in plugin with the `googleapis` package.
+
+## Getting Started
+
+For help getting started with Flutter, view our online
+[documentation](https://flutter.dev/).
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/build.gradle b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/build.gradle
new file mode 100755
index 0000000..2952c3b
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/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.googlesigninexample"
+ minSdkVersion 16
+ targetSdkVersion 28
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig signingConfigs.debug
+ }
+ }
+
+ testOptions {
+ unitTests.returnDefaultValues = true
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {
+ implementation 'com.google.android.gms:play-services-auth:16.0.1'
+ testImplementation'junit:junit:4.12'
+ testImplementation 'org.mockito:mockito-core:2.17.0'
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/google-services.json b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/google-services.json
new file mode 100644
index 0000000..efa5245
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/google-services.json
@@ -0,0 +1,246 @@
+{
+ "project_info": {
+ "project_number": "479882132969",
+ "firebase_url": "https://my-flutter-proj.firebaseio.com",
+ "project_id": "my-flutter-proj",
+ "storage_bucket": "my-flutter-proj.appspot.com"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:479882132969:android:c73fd19ff7e2c0be",
+ "android_client_info": {
+ "package_name": "io.flutter.plugins.cameraexample"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs"
+ }
+ ],
+ "services": {
+ "analytics_service": {
+ "status": 1
+ },
+ "appinvite_service": {
+ "status": 1,
+ "other_platform_oauth_client": []
+ },
+ "ads_service": {
+ "status": 2
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:479882132969:android:632cdf3fc0a17139",
+ "android_client_info": {
+ "package_name": "io.flutter.plugins.firebasedynamiclinksexample"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "479882132969-32qusitiag53931ck80h121ajhlc5a7e.apps.googleusercontent.com",
+ "client_type": 1,
+ "android_info": {
+ "package_name": "io.flutter.plugins.firebasedynamiclinksexample",
+ "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0"
+ }
+ },
+ {
+ "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs"
+ }
+ ],
+ "services": {
+ "analytics_service": {
+ "status": 1
+ },
+ "appinvite_service": {
+ "status": 2,
+ "other_platform_oauth_client": [
+ {
+ "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.flutter.plugins.firebaseMlVisionExample"
+ }
+ }
+ ]
+ },
+ "ads_service": {
+ "status": 2
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:479882132969:android:ae50362b4bc06086",
+ "android_client_info": {
+ "package_name": "io.flutter.plugins.firebasemlvisionexample"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "479882132969-9pp74fkgmtvt47t9rikc1p861v7n85tn.apps.googleusercontent.com",
+ "client_type": 1,
+ "android_info": {
+ "package_name": "io.flutter.plugins.firebasemlvisionexample",
+ "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0"
+ }
+ },
+ {
+ "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs"
+ }
+ ],
+ "services": {
+ "analytics_service": {
+ "status": 1
+ },
+ "appinvite_service": {
+ "status": 2,
+ "other_platform_oauth_client": [
+ {
+ "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.flutter.plugins.firebaseMlVisionExample"
+ }
+ }
+ ]
+ },
+ "ads_service": {
+ "status": 2
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:479882132969:android:215a22700e1b466b",
+ "android_client_info": {
+ "package_name": "io.flutter.plugins.firebaseperformanceexample"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "479882132969-8h4kiv8m7ho4tvn6uuujsfcrf69unuf7.apps.googleusercontent.com",
+ "client_type": 1,
+ "android_info": {
+ "package_name": "io.flutter.plugins.firebaseperformanceexample",
+ "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0"
+ }
+ },
+ {
+ "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs"
+ }
+ ],
+ "services": {
+ "analytics_service": {
+ "status": 1
+ },
+ "appinvite_service": {
+ "status": 2,
+ "other_platform_oauth_client": [
+ {
+ "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.flutter.plugins.firebaseMlVisionExample"
+ }
+ }
+ ]
+ },
+ "ads_service": {
+ "status": 2
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:479882132969:android:5e9f1f89e134dc86",
+ "android_client_info": {
+ "package_name": "io.flutter.plugins.googlesigninexample"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "479882132969-90ml692hkonp587sl0v0rurmnvkekgrg.apps.googleusercontent.com",
+ "client_type": 1,
+ "android_info": {
+ "package_name": "io.flutter.plugins.googlesigninexample",
+ "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0"
+ }
+ },
+ {
+ "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs"
+ }
+ ],
+ "services": {
+ "analytics_service": {
+ "status": 1
+ },
+ "appinvite_service": {
+ "status": 2,
+ "other_platform_oauth_client": [
+ {
+ "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.flutter.plugins.firebaseMlVisionExample"
+ }
+ }
+ ]
+ },
+ "ads_service": {
+ "status": 2
+ }
+ }
+ }
+ ],
+ "configuration_version": "1"
+}
\ No newline at end of file
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..9a4163a
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/AndroidManifest.xml b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/AndroidManifest.xml
new file mode 100755
index 0000000..df80f82
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="io.flutter.plugins.googlesigninexample">
+
+ <uses-permission android:name="android.permission.INTERNET"/>
+
+ <application>
+ <activity android:name="io.flutter.embedding.android.FlutterActivity"
+ android:theme="@android:style/Theme.Black.NoTitleBar"
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
+ android:hardwareAccelerated="true"
+ android:windowSoftInputMode="adjustResize">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <activity android:name=".EmbeddingV1Activity"
+ android:theme="@android:style/Theme.Black.NoTitleBar"
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
+ android:hardwareAccelerated="true"
+ android:windowSoftInputMode="adjustResize">
+ </activity>
+ <meta-data android:name="flutterEmbedding" android:value="2"/>
+ </application>
+</manifest>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/.gitignore b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/.gitignore
new file mode 100755
index 0000000..9eb4563
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/.gitignore
@@ -0,0 +1 @@
+GeneratedPluginRegistrant.java
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java
new file mode 100644
index 0000000..5ec1982
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java
@@ -0,0 +1,19 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.googlesigninexample;
+
+import android.os.Bundle;
+import io.flutter.plugins.googlesignin.GoogleSignInPlugin;
+import io.flutter.view.FlutterMain;
+
+@SuppressWarnings("deprecation")
+public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ FlutterMain.startInitialization(this);
+ super.onCreate(savedInstanceState);
+ GoogleSignInPlugin.registerWith(registrarFor("io.flutter.plugins.googlesignin"));
+ }
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java
new file mode 100644
index 0000000..3e7250c
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java
@@ -0,0 +1,18 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.googlesigninexample;
+
+import androidx.test.rule.ActivityTestRule;
+import dev.flutter.plugins.integration_test.FlutterTestRunner;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(FlutterTestRunner.class)
+@SuppressWarnings("deprecation")
+public class EmbeddingV1ActivityTest {
+ @Rule
+ public ActivityTestRule<EmbeddingV1Activity> rule =
+ new ActivityTestRule<>(EmbeddingV1Activity.class);
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java
new file mode 100644
index 0000000..f9aa77b
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java
@@ -0,0 +1,17 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.googlesigninexample;
+
+import androidx.test.rule.ActivityTestRule;
+import dev.flutter.plugins.integration_test.FlutterTestRunner;
+import io.flutter.embedding.android.FlutterActivity;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(FlutterTestRunner.class)
+public class FlutterActivityTest {
+ @Rule
+ public ActivityTestRule<FlutterActivity> rule = new ActivityTestRule<>(FlutterActivity.class);
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100755
index 0000000..db77bb4
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100755
index 0000000..17987b7
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100755
index 0000000..09d4391
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100755
index 0000000..d5f1c8d
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100755
index 0000000..4d6372e
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/values/strings.xml b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..c7e28ff
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="default_web_client_id">YOUR_WEB_CLIENT_ID</string>
+</resources>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/build.gradle b/packages/extension_google_sign_in_as_googleapis_auth/example/android/build.gradle
new file mode 100755
index 0000000..541636c
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/build.gradle
@@ -0,0 +1,29 @@
+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/extension_google_sign_in_as_googleapis_auth/example/android/gradle.properties b/packages/extension_google_sign_in_as_googleapis_auth/example/android/gradle.properties
new file mode 100755
index 0000000..38c8d45
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/extension_google_sign_in_as_googleapis_auth/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..019065d
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/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-4.10.2-all.zip
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/settings.gradle b/packages/extension_google_sign_in_as_googleapis_auth/example/android/settings.gradle
new file mode 100755
index 0000000..115da6c
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/settings.gradle
@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+ pluginsFile.withInputStream { stream -> plugins.load(stream) }
+}
+
+plugins.each { name, path ->
+ def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+ include ":$name"
+ project(":$name").projectDir = pluginDirectory
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/AppFrameworkInfo.plist b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/AppFrameworkInfo.plist
new file mode 100755
index 0000000..6c2de80
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>App</string>
+ <key>CFBundleIdentifier</key>
+ <string>io.flutter.flutter.app</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>App</string>
+ <key>CFBundlePackageType</key>
+ <string>FMWK</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>arm64</string>
+ </array>
+ <key>MinimumOSVersion</key>
+ <string>8.0</string>
+</dict>
+</plist>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Debug.xcconfig b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Debug.xcconfig
new file mode 100755
index 0000000..9803018
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "Generated.xcconfig"
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Release.xcconfig b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Release.xcconfig
new file mode 100755
index 0000000..a4a8c60
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "Generated.xcconfig"
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/GoogleSignInPluginTest/Info.plist b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/GoogleSignInPluginTest/Info.plist
new file mode 100644
index 0000000..64d65ca
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/GoogleSignInPluginTest/Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.pbxproj b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..faaaa58
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,502 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 5C6F5A6E1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6F5A6D1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m */; };
+ 7A303C2E1E89D76400B1F19E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7A303C2D1E89D76400B1F19E /* GoogleService-Info.plist */; };
+ 7ACDFB0E1E8944C400BE2D00 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7ACDFB0D1E8944C400BE2D00 /* AppFrameworkInfo.plist */; };
+ 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
+ 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+ C2FB9CBA01DB0A2DE5F31E12 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0263E28FA425D1CE928BDE15 /* libPods-Runner.a */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 0263E28FA425D1CE928BDE15 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 5A76713E622F06379AEDEBFA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
+ 5C6F5A6C1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
+ 5C6F5A6D1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
+ 7A303C2D1E89D76400B1F19E /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
+ 7ACDFB0D1E8944C400BE2D00 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
+ 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
+ 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
+ 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 = "<group>"; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ 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 = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ C2FB9CBA01DB0A2DE5F31E12 /* libPods-Runner.a in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 840012C8B5EDBCF56B0E4AC1 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 5A76713E622F06379AEDEBFA /* Pods-Runner.debug.xcconfig */,
+ F582639B44581540871D9BB0 /* Pods-Runner.release.xcconfig */,
+ );
+ name = Pods;
+ sourceTree = "<group>";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 7ACDFB0D1E8944C400BE2D00 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "<group>";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 840012C8B5EDBCF56B0E4AC1 /* Pods */,
+ CF3B75C9A7D2FA2A4C99F110 /* Frameworks */,
+ );
+ sourceTree = "<group>";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 5C6F5A6C1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.h */,
+ 5C6F5A6D1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m */,
+ 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
+ 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 7A303C2D1E89D76400B1F19E /* GoogleService-Info.plist */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 97C146F11CF9000F007C117D /* Supporting Files */,
+ );
+ path = Runner;
+ sourceTree = "<group>";
+ };
+ 97C146F11CF9000F007C117D /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146F21CF9000F007C117D /* main.m */,
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+ CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 0263E28FA425D1CE928BDE15 /* libPods-Runner.a */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */,
+ 532EA9D341340B1DCD08293D /* [CP] Copy Pods Resources */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1100;
+ ORGANIZATIONNAME = "The Chromium Authors";
+ TargetAttributes = {
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 7A303C2E1E89D76400B1F19E /* GoogleService-Info.plist in Resources */,
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 7ACDFB0E1E8944C400BE2D00 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin\n";
+ };
+ 532EA9D341340B1DCD08293D /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh",
+ "${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
+ "${PODS_ROOT}/../Flutter/Flutter.framework",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+ AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
+ 97C146F31CF9000F007C117D /* main.m in Sources */,
+ 5C6F5A6E1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "<group>";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ENABLE_BITCODE = NO;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.googleSignInExample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ENABLE_BITCODE = NO;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.googleSignInExample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100755
index 0000000..21a3cc1
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "group:Runner.xcodeproj">
+ </FileRef>
+ <FileRef
+ location = "group:Pods/Pods.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100755
index 0000000..3bb3697
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "1100"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <Testables>
+ </Testables>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100755
index 0000000..21a3cc1
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "group:Runner.xcodeproj">
+ </FileRef>
+ <FileRef
+ location = "group:Pods/Pods.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.h b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.h
new file mode 100644
index 0000000..d9e18e9
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.h
@@ -0,0 +1,10 @@
+// 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 <Flutter/Flutter.h>
+#import <UIKit/UIKit.h>
+
+@interface AppDelegate : FlutterAppDelegate
+
+@end
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.m b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.m
new file mode 100644
index 0000000..f086757
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.m
@@ -0,0 +1,17 @@
+// 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/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100755
index 0000000..d22f10b
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,116 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100755
index 0000000..28c6bf0
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100755
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100755
index 0000000..f091b6b
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100755
index 0000000..4cde121
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100755
index 0000000..d0ef06e
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100755
index 0000000..dcdc230
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100755
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100755
index 0000000..c8f9ed8
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100755
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100755
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100755
index 0000000..75b2d16
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100755
index 0000000..c4df70d
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100755
index 0000000..6a84f41
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100755
index 0000000..d0e1f58
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100755
index 0000000..ebf48f6
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+ <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/Main.storyboard b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/Main.storyboard
new file mode 100755
index 0000000..f3c2851
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ </dependencies>
+ <scenes>
+ <!--Flutter View Controller-->
+ <scene sceneID="tne-QT-ifu">
+ <objects>
+ <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+ <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+ </objects>
+ </scene>
+ </scenes>
+</document>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/GoogleService-Info.plist b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/GoogleService-Info.plist
new file mode 100644
index 0000000..6042aab
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/GoogleService-Info.plist
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>AD_UNIT_ID_FOR_BANNER_TEST</key>
+ <string>ca-app-pub-3940256099942544/2934735716</string>
+ <key>AD_UNIT_ID_FOR_INTERSTITIAL_TEST</key>
+ <string>ca-app-pub-3940256099942544/4411468910</string>
+ <key>CLIENT_ID</key>
+ <string>479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com</string>
+ <key>REVERSED_CLIENT_ID</key>
+ <string>com.googleusercontent.apps.479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u</string>
+ <key>ANDROID_CLIENT_ID</key>
+ <string>479882132969-jie8r1me6dsra60pal6ejaj8dgme3tg0.apps.googleusercontent.com</string>
+ <key>API_KEY</key>
+ <string>AIzaSyBECOwLTAN6PU4Aet1b2QLGIb3kRK8Xjew</string>
+ <key>GCM_SENDER_ID</key>
+ <string>479882132969</string>
+ <key>PLIST_VERSION</key>
+ <string>1</string>
+ <key>BUNDLE_ID</key>
+ <string>io.flutter.plugins.googleSignInExample</string>
+ <key>PROJECT_ID</key>
+ <string>my-flutter-proj</string>
+ <key>STORAGE_BUCKET</key>
+ <string>my-flutter-proj.appspot.com</string>
+ <key>IS_ADS_ENABLED</key>
+ <true></true>
+ <key>IS_ANALYTICS_ENABLED</key>
+ <false></false>
+ <key>IS_APPINVITE_ENABLED</key>
+ <true></true>
+ <key>IS_GCM_ENABLED</key>
+ <true></true>
+ <key>IS_SIGNIN_ENABLED</key>
+ <true></true>
+ <key>GOOGLE_APP_ID</key>
+ <string>1:479882132969:ios:2643f950e0a0da08</string>
+ <key>DATABASE_URL</key>
+ <string>https://my-flutter-proj.firebaseio.com</string>
+ <key>SERVER_CLIENT_ID</key>
+ <string>YOUR_SERVER_CLIENT_ID</string>
+</dict>
+</plist>
\ No newline at end of file
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist
new file mode 100755
index 0000000..e03ccfe
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleDisplayName</key>
+ <string>Google Sign-In Example</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>GoogleSignInExample</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleURLTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Editor</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>com.googleusercontent.apps.479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u</string>
+ </array>
+ </dict>
+ </array>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>arm64</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UIViewControllerBasedStatusBarAppearance</key>
+ <false/>
+</dict>
+</plist>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/main.m b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/main.m
new file mode 100644
index 0000000..bec320c
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/main.m
@@ -0,0 +1,13 @@
+// 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 <Flutter/Flutter.h>
+#import <UIKit/UIKit.h>
+#import "AppDelegate.h"
+
+int main(int argc, char* argv[]) {
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
+ }
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart b/packages/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart
new file mode 100755
index 0000000..a7ebc09
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart
@@ -0,0 +1,150 @@
+// 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 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:google_sign_in/google_sign_in.dart';
+
+import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart';
+import 'package:googleapis/people/v1.dart';
+
+final GoogleSignIn _googleSignIn = GoogleSignIn(
+ scopes: <String>[PeopleServiceApi.contactsReadonlyScope],
+);
+
+void main() {
+ runApp(
+ const MaterialApp(
+ title: 'Google Sign In',
+ home: SignInDemo(),
+ ),
+ );
+}
+
+/// The main widget of this demo.
+class SignInDemo extends StatefulWidget {
+ /// Creates the main widget of this demo.
+ const SignInDemo({Key? key}) : super(key: key);
+
+ @override
+ State createState() => SignInDemoState();
+}
+
+/// The state of the main widget.
+class SignInDemoState extends State<SignInDemo> {
+ GoogleSignInAccount? _currentUser;
+ String? _contactText;
+
+ @override
+ void initState() {
+ super.initState();
+ _googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount? account) {
+ setState(() {
+ _currentUser = account;
+ });
+ if (_currentUser != null) {
+ _handleGetContact();
+ }
+ });
+ _googleSignIn.signInSilently();
+ }
+
+ Future<void> _handleGetContact() async {
+ setState(() {
+ _contactText = 'Loading contact info...';
+ });
+
+ final PeopleServiceApi peopleApi =
+ PeopleServiceApi((await _googleSignIn.authenticatedClient())!);
+ final ListConnectionsResponse response =
+ await peopleApi.people.connections.list(
+ 'people/me',
+ personFields: 'names',
+ );
+
+ final String? firstNamedContactName =
+ _pickFirstNamedContact(response.connections);
+
+ setState(() {
+ if (firstNamedContactName != null) {
+ _contactText = 'I see you know $firstNamedContactName!';
+ } else {
+ _contactText = 'No contacts to display.';
+ }
+ });
+ }
+
+ String? _pickFirstNamedContact(List<Person>? connections) {
+ return connections
+ ?.firstWhere(
+ (Person person) => person.names != null,
+ )
+ .names
+ ?.firstWhere(
+ (Name name) => name.displayName != null,
+ )
+ .displayName;
+ }
+
+ Future<void> _handleSignIn() async {
+ try {
+ await _googleSignIn.signIn();
+ } catch (error) {
+ print(error);
+ }
+ }
+
+ Future<void> _handleSignOut() => _googleSignIn.disconnect();
+
+ Widget _buildBody() {
+ if (_currentUser != null) {
+ return Column(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: <Widget>[
+ ListTile(
+ leading: GoogleUserCircleAvatar(
+ identity: _currentUser!,
+ ),
+ title: Text(_currentUser!.displayName ?? ''),
+ subtitle: Text(_currentUser!.email),
+ ),
+ const Text('Signed in successfully.'),
+ Text(_contactText ?? ''),
+ ElevatedButton(
+ child: const Text('SIGN OUT'),
+ onPressed: _handleSignOut,
+ ),
+ ElevatedButton(
+ child: const Text('REFRESH'),
+ onPressed: _handleGetContact,
+ ),
+ ],
+ );
+ } else {
+ return Column(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: <Widget>[
+ const Text('You are not currently signed in.'),
+ ElevatedButton(
+ child: const Text('SIGN IN'),
+ onPressed: _handleSignIn,
+ ),
+ ],
+ );
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Google Sign In'),
+ ),
+ body: ConstrainedBox(
+ constraints: const BoxConstraints.expand(),
+ child: _buildBody(),
+ ));
+ }
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml b/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml
new file mode 100644
index 0000000..b5ed9a1
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml
@@ -0,0 +1,26 @@
+name: extension_google_sign_in_example
+description: Example of Google Sign-In plugin and googleapis.
+publish_to: none
+
+dependencies:
+ extension_google_sign_in_as_googleapis_auth:
+ # When depending on this package from a real application you should use:
+ # extension_google_sign_in_as_googleapis_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: ../
+ flutter:
+ sdk: flutter
+ google_sign_in: ^5.0.0
+ googleapis: ^2.0.0
+
+dev_dependencies:
+ pedantic: ^1.10.0
+
+flutter:
+ uses-material-design: true
+
+environment:
+ sdk: ">=2.12.0 <3.0.0"
+ flutter: ">=1.20.0"
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/web/index.html b/packages/extension_google_sign_in_as_googleapis_auth/example/web/index.html
new file mode 100644
index 0000000..42a7d93
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/web/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <meta name="google-signin-client_id" content="[YOUR_OAUTH_2_CLIENT_ID_FOR_WEB]" />
+ <title>Google Sign-in Example</title>
+</head>
+<body>
+ <script src="main.dart.js" type="application/javascript"></script>
+</body>
+</html>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart b/packages/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart
new file mode 100644
index 0000000..af81105
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart
@@ -0,0 +1,42 @@
+// Copyright 2020 The Flutter Authors
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+import 'package:google_sign_in/google_sign_in.dart';
+import 'package:googleapis_auth/googleapis_auth.dart' as gapis;
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+
+/// Extension on [GoogleSignIn] that adds an `authenticatedClient` method.
+///
+/// This method can be used to retrieve an authenticated [gapis.AuthClient]
+/// client that can be used with the rest of the `googleapis` libraries.
+extension GoogleApisGoogleSignInAuth on GoogleSignIn {
+ /// Retrieve a `googleapis` authenticated client.
+ Future<gapis.AuthClient?> authenticatedClient({
+ @visibleForTesting GoogleSignInAuthentication? debugAuthentication,
+ @visibleForTesting List<String>? debugScopes,
+ }) async {
+ final GoogleSignInAuthentication? auth =
+ debugAuthentication ?? await currentUser?.authentication;
+ final String? oathTokenString = auth?.accessToken;
+ if (oathTokenString == null) {
+ return null;
+ }
+ final gapis.AccessCredentials credentials = gapis.AccessCredentials(
+ gapis.AccessToken(
+ 'Bearer',
+ oathTokenString,
+ // TODO(kevmoo): Use the correct value once it's available from authentication
+ // See https://github.com/flutter/flutter/issues/80905
+ DateTime.now().toUtc().add(const Duration(days: 365)),
+ ),
+ null, // We don't have a refreshToken
+ debugScopes ?? scopes,
+ );
+
+ return gapis.authenticatedClient(http.Client(), credentials);
+ }
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml b/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml
new file mode 100644
index 0000000..0b6cfb1
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml
@@ -0,0 +1,27 @@
+# Copyright 2020 The Flutter Authors
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+name: extension_google_sign_in_as_googleapis_auth
+description: A bridge package between google_sign_in and googleapis_auth, to create Authenticated Clients from google_sign_in user credentials.
+version: 2.0.2
+repository: https://github.com/flutter/packages/tree/master/packages/extension_google_sign_in_as_googleapis_auth
+
+dependencies:
+ flutter:
+ sdk: flutter
+ google_sign_in: ^5.0.0
+ googleapis_auth: ^1.1.0
+ http: ^0.13.0
+ meta: ^1.3.0
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+ pedantic: ^1.10.0
+
+environment:
+ sdk: ">=2.12.0 <3.0.0"
+ flutter: ">=1.20.0"
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart b/packages/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart
new file mode 100644
index 0000000..ac43b35
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart
@@ -0,0 +1,59 @@
+// Copyright 2020 The Flutter Authors
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+import 'package:google_sign_in/google_sign_in.dart';
+import 'package:googleapis_auth/googleapis_auth.dart' as gapis;
+import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+const String SOME_FAKE_ACCESS_TOKEN = 'this-is-something-not-null';
+const List<String> DEBUG_FAKE_SCOPES = <String>['some-scope', 'another-scope'];
+const List<String> SIGN_IN_FAKE_SCOPES = <String>[
+ 'some-scope',
+ 'another-scope'
+];
+
+class FakeGoogleSignIn extends Fake implements GoogleSignIn {
+ @override
+ final List<String> scopes = SIGN_IN_FAKE_SCOPES;
+}
+
+class FakeGoogleSignInAuthentication extends Fake
+ implements GoogleSignInAuthentication {
+ @override
+ final String accessToken = SOME_FAKE_ACCESS_TOKEN;
+}
+
+void main() {
+ final GoogleSignIn signIn = FakeGoogleSignIn();
+ final FakeGoogleSignInAuthentication authMock =
+ FakeGoogleSignInAuthentication();
+
+ test('authenticatedClient returns an authenticated client', () async {
+ final gapis.AuthClient client = (await signIn.authenticatedClient(
+ debugAuthentication: authMock,
+ ))!;
+ expect(client, isA<gapis.AuthClient>());
+ });
+
+ test('authenticatedClient uses GoogleSignIn scopes by default', () async {
+ final gapis.AuthClient client = (await signIn.authenticatedClient(
+ debugAuthentication: authMock,
+ ))!;
+ expect(client.credentials.accessToken.data, equals(SOME_FAKE_ACCESS_TOKEN));
+ expect(client.credentials.scopes, equals(SIGN_IN_FAKE_SCOPES));
+ });
+
+ test('authenticatedClient returned client contains the passed-in credentials',
+ () async {
+ final gapis.AuthClient client = (await signIn.authenticatedClient(
+ debugAuthentication: authMock,
+ debugScopes: DEBUG_FAKE_SCOPES,
+ ))!;
+ expect(client.credentials.accessToken.data, equals(SOME_FAKE_ACCESS_TOKEN));
+ expect(client.credentials.scopes, equals(DEBUG_FAKE_SCOPES));
+ });
+}
diff --git a/packages/flutter_template_images/.gitignore b/packages/flutter_template_images/.gitignore
new file mode 100644
index 0000000..9e04949
--- /dev/null
+++ b/packages/flutter_template_images/.gitignore
@@ -0,0 +1,34 @@
+# 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/
+
+# Exceptions to above rules.
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/flutter_template_images/CHANGELOG.md b/packages/flutter_template_images/CHANGELOG.md
new file mode 100644
index 0000000..7442d0b
--- /dev/null
+++ b/packages/flutter_template_images/CHANGELOG.md
@@ -0,0 +1,12 @@
+## 2.0.0
+
+* Move assets common to all app templates to a new `app_shared` directory.
+* Create `list_detail_app` directory and assets to support new app template.
+
+## 1.0.1
+
+* Moved Windows app template icon for new folder structure.
+
+## 1.0.0
+
+* Windows app template icon.
diff --git a/packages/flutter_template_images/LICENSE b/packages/flutter_template_images/LICENSE
new file mode 100644
index 0000000..aea51a3
--- /dev/null
+++ b/packages/flutter_template_images/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2014 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_template_images/README.md b/packages/flutter_template_images/README.md
new file mode 100644
index 0000000..3ae9dc9
--- /dev/null
+++ b/packages/flutter_template_images/README.md
@@ -0,0 +1,9 @@
+# flutter\_template\_images
+
+Images used by the `flutter_tools` templates.
+
+This project is an internal dependency of the `flutter` tool, and is
+not intended to be used directly. It contains images files used in
+`flutter create` templates, to avoid checking them into [the main
+Flutter repository](https://github.com/flutter/flutter), where they would
+permanently increase the checkout size over time if altered.
diff --git a/packages/flutter_template_images/lib/flutter_template_images.dart b/packages/flutter_template_images/lib/flutter_template_images.dart
new file mode 100644
index 0000000..fc5fff7
--- /dev/null
+++ b/packages/flutter_template_images/lib/flutter_template_images.dart
@@ -0,0 +1,3 @@
+library flutter_template_images;
+
+// There is no source for this package, since it only exists to hold images.
diff --git a/packages/flutter_template_images/pubspec.yaml b/packages/flutter_template_images/pubspec.yaml
new file mode 100644
index 0000000..8b51fb2
--- /dev/null
+++ b/packages/flutter_template_images/pubspec.yaml
@@ -0,0 +1,7 @@
+name: flutter_template_images
+description: Image files for use in flutter_tools templates.
+version: 2.0.0
+homepage: https://github.com/flutter/packages/tree/master/packages/flutter_template_images
+
+environment:
+ sdk: ">=2.1.0 <3.0.0"
diff --git a/packages/flutter_template_images/templates/app_shared/web/icons/Icon-maskable-192.png.copy.tmpl b/packages/flutter_template_images/templates/app_shared/web/icons/Icon-maskable-192.png.copy.tmpl
new file mode 100644
index 0000000..c9236db
--- /dev/null
+++ b/packages/flutter_template_images/templates/app_shared/web/icons/Icon-maskable-192.png.copy.tmpl
Binary files differ
diff --git a/packages/flutter_template_images/templates/app_shared/web/icons/Icon-maskable-512.png.copy.tmpl b/packages/flutter_template_images/templates/app_shared/web/icons/Icon-maskable-512.png.copy.tmpl
new file mode 100644
index 0000000..4068c4e
--- /dev/null
+++ b/packages/flutter_template_images/templates/app_shared/web/icons/Icon-maskable-512.png.copy.tmpl
Binary files differ
diff --git a/packages/flutter_template_images/templates/app_shared/windows.tmpl/runner/resources/app_icon.ico b/packages/flutter_template_images/templates/app_shared/windows.tmpl/runner/resources/app_icon.ico
new file mode 100644
index 0000000..c04e20c
--- /dev/null
+++ b/packages/flutter_template_images/templates/app_shared/windows.tmpl/runner/resources/app_icon.ico
Binary files differ
diff --git a/packages/flutter_template_images/templates/list_detail_app/assets/images/2.0x/flutter_logo.png b/packages/flutter_template_images/templates/list_detail_app/assets/images/2.0x/flutter_logo.png
new file mode 100644
index 0000000..b65164d
--- /dev/null
+++ b/packages/flutter_template_images/templates/list_detail_app/assets/images/2.0x/flutter_logo.png
Binary files differ
diff --git a/packages/flutter_template_images/templates/list_detail_app/assets/images/3.0x/flutter_logo.png b/packages/flutter_template_images/templates/list_detail_app/assets/images/3.0x/flutter_logo.png
new file mode 100644
index 0000000..97e5dc9
--- /dev/null
+++ b/packages/flutter_template_images/templates/list_detail_app/assets/images/3.0x/flutter_logo.png
Binary files differ
diff --git a/packages/flutter_template_images/templates/list_detail_app/assets/images/flutter_logo.png b/packages/flutter_template_images/templates/list_detail_app/assets/images/flutter_logo.png
new file mode 100644
index 0000000..b5c6ca7
--- /dev/null
+++ b/packages/flutter_template_images/templates/list_detail_app/assets/images/flutter_logo.png
Binary files differ
diff --git a/packages/fuchsia_ctl/.gitignore b/packages/fuchsia_ctl/.gitignore
new file mode 100644
index 0000000..851938e
--- /dev/null
+++ b/packages/fuchsia_ctl/.gitignore
@@ -0,0 +1,18 @@
+# CIPD and Dart-SDK binaries
+.cipd/
+dart-sdk/
+
+# Build outputs
+*.aot
+*.dill
+.dart_tool/
+build/
+.ssh/
+
+# Fuchsia SDK binaries someone might move in for testing
+device-finder
+pm
+
+# Dart
+.packages
+!pubspec.lock
diff --git a/packages/fuchsia_ctl/CHANGELOG.md b/packages/fuchsia_ctl/CHANGELOG.md
new file mode 100644
index 0000000..43716b9
--- /dev/null
+++ b/packages/fuchsia_ctl/CHANGELOG.md
@@ -0,0 +1,76 @@
+# CHANGELOG
+
+## version:0.0.27
+
+- Flush the content of iosink when writing output to a file.
+
+## version:0.0.26
+
+- Replace amberctl with pkgctl.
+
+## version:0.0.25
+
+- Added log-file option to stream logs to a file.
+
+
+## 0.0.24
+
+- Use fuchsia sdk `device-finder` instead of `dev_finder`.
+
+## 0.0.23
+
+- Added `emu` tool for spawning an emulator instance given a Fuchsia QEMU build,
+ the Fuchsia SDK, and an Android emulator executable.
+- Fixed homepage link.
+
+## 0.0.18 - 0.0.22
+
+- Add retries to paving operations.
+- Add timeouts to paving and ssh operations.
+- Add arguments parameter to test command.
+
+## 0.0.17
+
+- Add "push-packages" option.
+
+## 0.0.16
+
+- Refactor usages of `amberctl` to its own class.
+
+## 0.0.9 - 0.0.15
+
+- Various improvements including support for Fuchsia tests.
+
+## 0.0.8
+
+- Use fuchsiapkg URLs to launch test target
+
+## 0.0.7
+
+- Set error code in wrapper script
+
+## 0.0.6
+
+- Use random port for PM serving.
+- Add more error logging on failures.
+
+## 0.0.5
+
+- Fix SSH command input so that correct exit code is returned when a command
+ fails.
+
+## 0.0.4
+
+- Fix issue where tests results were not printing.
+
+## 0.0.3
+
+- Publish correct binaries for Linux
+
+## 0.0.2
+
+- Published with mac binaries tagged as Linux. Do not use.
+
+## 0.0.1
+
+- Initial pre-release for testing. Do not use.
diff --git a/packages/fuchsia_ctl/LICENSE b/packages/fuchsia_ctl/LICENSE
new file mode 100644
index 0000000..8211a02
--- /dev/null
+++ b/packages/fuchsia_ctl/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014 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/fuchsia_ctl/README.md b/packages/fuchsia_ctl/README.md
new file mode 100644
index 0000000..e14fbb1
--- /dev/null
+++ b/packages/fuchsia_ctl/README.md
@@ -0,0 +1,29 @@
+# fuchsia_ctl
+
+This package is used by Flutter CI systems to manage paving and testing Fuchsia
+devices.
+
+It offers some functionality similar to the `fx` command in the Fuchsia SDK.
+
+It is not intended for general use.
+
+## Building
+
+This tool is meant to be published as an AOT compiled binary distributed via
+CIPD. To build the AOT binary, run `tool/build.sh`. This must be done on a
+Linux machine, and will automatically download a suitable version of Dart to
+build the binary.
+
+To create the CIPD package, make sure that the `build/` folder does not contain
+any files from testing (e.g. a generated `.ssh` folder from paving or a copy of
+`device-finder` or `pm`). Then run:
+
+```bash
+cipd create -in build \
+ -name flutter/fuchsia_ctl/linux-amd64 \
+ -ref stable \
+ -tag version:n.n.n
+```
+
+with an appropriate version string, after running `tool/build.sh`.
+
diff --git a/packages/fuchsia_ctl/bin/main.dart b/packages/fuchsia_ctl/bin/main.dart
new file mode 100644
index 0000000..014fa30
--- /dev/null
+++ b/packages/fuchsia_ctl/bin/main.dart
@@ -0,0 +1,408 @@
+// 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 'dart:io';
+
+import 'package:args/args.dart';
+import 'package:file/file.dart';
+import 'package:file/local.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as path;
+import 'package:retry/retry.dart';
+import 'package:uuid/uuid.dart';
+
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+
+typedef AsyncResult = Future<OperationResult> Function(
+ String, DevFinder, ArgResults);
+
+const Map<String, AsyncResult> commands = <String, AsyncResult>{
+ 'emu': emulator,
+ 'pave': pave,
+ 'pm': pm,
+ 'ssh': ssh,
+ 'test': test,
+ 'push-packages': pushPackages,
+};
+
+Future<void> main(List<String> args) async {
+ if (!Platform.isLinux) {
+ throw UnsupportedError('This tool only supports Linux.');
+ }
+
+ final ArgParser parser = ArgParser();
+ parser
+ ..addOption('device-name',
+ abbr: 'd',
+ help: 'The device node name to use. '
+ 'If not specified, the first discoverable device will be used.')
+ ..addOption('device-finder-path',
+ defaultsTo: './device-finder',
+ help: 'The path to the device-finder executable.')
+ ..addFlag('help', defaultsTo: false, help: 'Prints help.');
+
+ /// This is a blocking command and will run until exited.
+ parser.addCommand('emu')
+ ..addOption('image', help: 'Fuchsia image to run')
+ ..addOption('zbi', help: 'Bootloader image to sign and run')
+ ..addOption('qemu-kernel', help: 'QEMU kernel to run')
+ ..addOption('window-size', help: 'Emulator window size formatted "WxH"')
+ ..addOption('aemu', help: 'AEMU executable path')
+ ..addOption('sdk',
+ help: 'Location to Fuchsia SDK containing tools and images')
+ ..addOption('public-key',
+ defaultsTo: '.fuchsia/authorized_keys',
+ help: 'Path to the authorized_keys to sign zbi image with')
+ ..addFlag('headless', help: 'Run FEMU without graphical window');
+
+ parser.addCommand('ssh')
+ ..addFlag('interactive',
+ abbr: 'i',
+ help: 'Whether to ssh in interactive mode. '
+ 'If --comand is specified, this is ignored.')
+ ..addOption('command',
+ abbr: 'c',
+ help: 'The command to run on the device. '
+ 'If specified, --interactive is ignored.')
+ ..addOption('identity-file',
+ defaultsTo: '.ssh/pkey', help: 'The key to use when SSHing.')
+ ..addOption('timeout-seconds',
+ defaultsTo: '120', help: 'Ssh command timeout in seconds.')
+ ..addOption('log-file',
+ defaultsTo: '', help: 'The file to write stdout and stderr.');
+ parser.addCommand('pave')
+ ..addOption('public-key',
+ abbr: 'p', help: 'The public key to add to authorized_keys.')
+ ..addOption('image',
+ abbr: 'i', help: 'The system image tgz to unpack and pave.');
+
+ final ArgParser pmSubCommand = parser.addCommand('pm')
+ ..addOption('pm-path',
+ defaultsTo: './pm', help: 'The path to the pm executable.')
+ ..addOption('repo',
+ abbr: 'r',
+ help: 'The location of the repository folder to create, '
+ 'publish, or serve.')
+ ..addCommand('serve')
+ ..addCommand('newRepo');
+ pmSubCommand
+ .addCommand('publishRepo')
+ .addMultiOption('far', abbr: 'f', help: 'The .far files to publish.');
+
+ parser.addCommand('push-packages')
+ ..addOption('pm-path',
+ defaultsTo: './pm', help: 'The path to the pm executable.')
+ ..addOption('repoArchive', help: 'The path to the repo tar.gz archive.')
+ ..addOption('identity-file',
+ defaultsTo: '.ssh/pkey', help: 'The key to use when SSHing.')
+ ..addMultiOption('packages',
+ abbr: 'p',
+ help: 'Packages from the repo that need to be pushed to the device.');
+
+ parser.addCommand('test')
+ ..addOption('pm-path',
+ defaultsTo: './pm', help: 'The path to the pm executable.')
+ ..addOption('identity-file',
+ defaultsTo: '.ssh/pkey', help: 'The key to use when SSHing.')
+ ..addOption('target',
+ abbr: 't', help: 'The name of the target to pass to runtests.')
+ ..addOption('arguments',
+ abbr: 'a',
+ help: 'Command line arguments to pass when invoking the tests')
+ ..addMultiOption('far',
+ abbr: 'f', help: 'The .far files to include for the test.')
+ ..addOption('timeout-seconds',
+ defaultsTo: '120', help: 'Test timeout in seconds.')
+ ..addOption('packages-directory', help: 'amber files directory.');
+
+ final ArgResults results = parser.parse(args);
+
+ if (results.command == null) {
+ stderr.writeln('Unknown command, expected one of: ${parser.commands.keys}');
+ stderr.writeln(parser.usage);
+ exit(-1);
+ }
+
+ if (results['help']) {
+ stderr.writeln(parser.commands[results.command.name].usage);
+ exit(0);
+ }
+
+ final AsyncResult command = commands[results.command.name];
+ if (command == null) {
+ stderr.writeln('Unkown command ${results.command.name}.');
+ stderr.writeln(parser.usage);
+ exit(-1);
+ }
+ final OperationResult result = await command(
+ results['device-name'],
+ DevFinder(results['device-finder-path']),
+ results.command,
+ );
+ if (!result.success) {
+ exit(-1);
+ }
+}
+
+@visibleForTesting
+Future<OperationResult> emulator(
+ String deviceName,
+ DevFinder devFinder,
+ ArgResults args,
+) async {
+ final Emulator emulator = Emulator(
+ aemuPath: args['aemu'],
+ fuchsiaImagePath: args['image'],
+ fuchsiaSdkPath: args['sdk'],
+ qemuKernelPath: args['qemu-kernel'],
+ sshKeyManager: SystemSshKeyManager.defaultProvider(
+ publicKeyPath: args['public-key'],
+ ),
+ zbiPath: args['zbi'],
+ );
+ await emulator.prepareEnvironment();
+
+ return emulator.start(
+ headless: args['headless'],
+ windowSize: args['window-size'],
+ );
+}
+
+@visibleForTesting
+Future<OperationResult> ssh(
+ String deviceName,
+ DevFinder devFinder,
+ ArgResults args,
+) async {
+ const SshClient sshClient = SshClient();
+ final String targetIp = await devFinder.getTargetAddress(deviceName);
+ final String identityFile = args['identity-file'];
+ final String outputFile = args['log-file'];
+ if (args['interactive']) {
+ return sshClient.interactive(
+ targetIp,
+ identityFilePath: identityFile,
+ );
+ }
+ final OperationResult result = await sshClient.runCommand(
+ targetIp,
+ identityFilePath: identityFile,
+ command: args['command'].split(' '),
+ timeoutMs:
+ Duration(milliseconds: int.parse(args['timeout-seconds']) * 1000),
+ logFilePath: outputFile,
+ );
+ stdout.writeln(
+ '==================================== STDOUT ====================================');
+ stdout.writeln(result.info);
+ stderr.writeln(
+ '==================================== STDERR ====================================');
+ stderr.writeln(result.error);
+ return result;
+}
+
+@visibleForTesting
+Future<OperationResult> pave(
+ String deviceName,
+ DevFinder devFinder,
+ ArgResults args,
+) async {
+ const ImagePaver paver = ImagePaver();
+ const RetryOptions r = RetryOptions(
+ maxDelay: Duration(seconds: 30),
+ maxAttempts: 3,
+ );
+ return r.retry(() async {
+ final OperationResult result = await paver.pave(
+ args['image'],
+ deviceName,
+ publicKeyPath: args['public-key'],
+ );
+ if (!result.success) {
+ throw RetryException('Exit code different from 0', result);
+ }
+ return result;
+ }, retryIf: (Exception e) => e is RetryException);
+}
+
+@visibleForTesting
+Future<OperationResult> pm(
+ String deviceName,
+ DevFinder devFinder,
+ ArgResults args,
+) async {
+ final PackageServer server = PackageServer(args['pm-path']);
+ switch (args.command.name) {
+ case 'serve':
+ await server.serveRepo(args['repo']);
+ await Future<void>.delayed(const Duration(seconds: 15));
+ return server.close();
+ case 'newRepo':
+ return server.newRepo(args['repo']);
+ case 'publishRepo':
+ return server.publishRepo(args['repo'], args['far']);
+ default:
+ throw ArgumentError('Command ${args.command.name} unknown.');
+ }
+}
+
+@visibleForTesting
+Future<OperationResult> pushPackages(
+ String deviceName,
+ DevFinder devFinder,
+ ArgResults args,
+) async {
+ final PackageServer server = PackageServer(args['pm-path']);
+ final String repoArchive = args['repoArchive'];
+ final List<String> packages = args['packages'];
+ final String identityFile = args['identity-file'];
+
+ const FileSystem fs = LocalFileSystem();
+ final String uuid = Uuid().v4();
+ final Directory repo = fs.systemTempDirectory.childDirectory('repo_$uuid');
+ const Tar tar = SystemTar();
+ try {
+ final String targetIp = await devFinder.getTargetAddress(deviceName);
+ final AmberCtl amberCtl = AmberCtl(targetIp, identityFile);
+
+ stdout.writeln('Untaring $repoArchive to ${repo.path}');
+ repo.createSync(recursive: true);
+ final OperationResult result = await tar.untar(repoArchive, repo.path);
+ if (!result.success) {
+ stdout.writeln(
+ 'Error untarring $repoArchive \nstdout: ${result.info} \nstderr: ${result.error}');
+ exit(-1);
+ }
+
+ final String repositoryBase = path.join(repo.path, 'amber-files');
+ stdout.writeln('Serving $repositoryBase to $targetIp');
+ await server.serveRepo(repositoryBase, port: 0);
+ await amberCtl.addSrc(server.serverPort);
+
+ stdout.writeln('Pushing packages $packages to $targetIp');
+ for (final String packageName in packages) {
+ stdout.writeln('Attempting to add package $packageName.');
+ await amberCtl.addPackage(packageName);
+ }
+
+ return OperationResult.success(
+ info: 'Successfully pushed $packages to $targetIp.');
+ } finally {
+ // We may not have created the repo if dev finder errored first.
+ if (repo.existsSync()) {
+ repo.deleteSync(recursive: true);
+ }
+ if (server.serving) {
+ await server.close();
+ }
+ }
+}
+
+@visibleForTesting
+Future<OperationResult> test(
+ String deviceName,
+ DevFinder devFinder,
+ ArgResults args,
+) async {
+ const FileSystem fs = LocalFileSystem();
+ final String identityFile = args['identity-file'];
+
+ //final PackageServer server = PackageServer(args['pm-path']);
+ PackageServer server;
+ const SshClient ssh = SshClient();
+ final List<String> farFiles = args['far'];
+ final String target = args['target'];
+ final String arguments = args['arguments'];
+ Directory repo;
+ if (args['packages-directory'] == null) {
+ final String uuid = Uuid().v4();
+ repo = fs.systemTempDirectory.childDirectory('repo_$uuid');
+ server = PackageServer(args['pm-path']);
+ } else {
+ final String amberFilesPath = path.join(
+ args['packages-directory'],
+ 'amber-files',
+ );
+ final String pmPath = path.join(
+ args['packages-directory'],
+ 'pm',
+ );
+ repo = fs.directory(amberFilesPath);
+ server = PackageServer(pmPath);
+ }
+
+ try {
+ final String targetIp = await devFinder.getTargetAddress(deviceName);
+ final AmberCtl amberCtl = AmberCtl(targetIp, identityFile);
+ OperationResult result;
+ stdout.writeln('Using ${repo.path} as repo to serve to $targetIp...');
+ if (!repo.existsSync()) {
+ repo.createSync(recursive: true);
+ result = await server.newRepo(repo.path);
+ if (!result.success) {
+ stderr.writeln('Failed to create repo at $repo.');
+ return result;
+ }
+ }
+ await server.serveRepo(repo.path, port: 0);
+ await amberCtl.addSrc(server.serverPort);
+
+ for (final String farFile in farFiles) {
+ result = await server.publishRepo(repo.path, farFile);
+ if (!result.success) {
+ stderr.writeln('Failed to publish repo at $repo with $farFiles.');
+ stderr.writeln(result.error);
+ return result;
+ }
+ final RegExp r = RegExp(r'\-0.far|.far');
+ final String packageName = fs.file(farFile).basename.replaceFirst(r, '');
+ await amberCtl.addPackage(packageName);
+ }
+
+ final OperationResult testResult = await ssh.runCommand(
+ targetIp,
+ identityFilePath: identityFile,
+ command: <String>[
+ 'run',
+ 'fuchsia-pkg://fuchsia.com/$target#meta/$target.cmx',
+ arguments
+ ],
+ timeoutMs:
+ Duration(milliseconds: int.parse(args['timeout-seconds']) * 1000),
+ );
+ stdout.writeln('Test results (passed: ${testResult.success}):');
+ if (result.info != null) {
+ stdout.writeln(testResult.info);
+ }
+ if (result.error != null) {
+ stderr.writeln(testResult.error);
+ }
+ return testResult;
+ } finally {
+ // We may not have created the repo if dev finder errored first.
+ if (repo.existsSync() && args['packages-directory'] != null) {
+ repo.deleteSync(recursive: true);
+ }
+ if (server.serving) {
+ await server.close();
+ }
+ }
+}
+
+/// The exception thrown when an operation needs a retry.
+class RetryException implements Exception {
+ /// Creates a new [RetryException] using the specified [cause] and [result]
+ /// to force a retry.
+ const RetryException(this.cause, this.result);
+
+ /// The user-facing message to display.
+ final String cause;
+
+ /// Contains the result of the executed target command.
+ final OperationResult result;
+
+ @override
+ String toString() =>
+ '$runtimeType, cause: "$cause", underlying exception: $result.';
+}
diff --git a/packages/fuchsia_ctl/lib/fuchsia_ctl.dart b/packages/fuchsia_ctl/lib/fuchsia_ctl.dart
new file mode 100644
index 0000000..209c81a
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/fuchsia_ctl.dart
@@ -0,0 +1,13 @@
+// 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.
+
+export 'src/amber_ctl.dart';
+export 'src/dev_finder.dart';
+export 'src/emulator.dart';
+export 'src/image_paver.dart';
+export 'src/operation_result.dart';
+export 'src/package_server.dart';
+export 'src/ssh_client.dart';
+export 'src/ssh_key_manager.dart';
+export 'src/tar.dart';
diff --git a/packages/fuchsia_ctl/lib/src/amber_ctl.dart b/packages/fuchsia_ctl/lib/src/amber_ctl.dart
new file mode 100644
index 0000000..8d0e6a4
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/amber_ctl.dart
@@ -0,0 +1,107 @@
+// 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 'dart:io';
+
+import 'package:meta/meta.dart';
+import 'package:uuid/uuid.dart';
+
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+
+const SshClient _kSsh = SshClient();
+
+/// Wrapper for amberctl utility for commands executed on the target device.
+@immutable
+class AmberCtl {
+ /// Creates a new [AmberCtl] with the specified [targetIp] and [identityFile].
+ const AmberCtl(
+ this._targetIp,
+ this._identityFile,
+ );
+
+ final String _identityFile;
+ final String _targetIp;
+
+ /// Adds a new package update source for the target device.
+ ///
+ /// * [port] is what "pm serve" is bound to.
+ /// * Returns the name of the package update source that is randomly generated.
+ Future<String> addSrc(int port) async {
+ final String uuid = Uuid().v4();
+ final String localIp = await _getLocalIp(_targetIp);
+ final List<String> addSource = <String>[
+ 'amberctl',
+ 'add_src',
+ '-f',
+ 'http://[$localIp]:$port/config.json',
+ '-n',
+ uuid,
+ ];
+
+ stdout.writeln('Adding amberctl source: ${addSource.join(' ')}');
+ final OperationResult result = await _kSsh.runCommand(
+ _targetIp,
+ identityFilePath: _identityFile,
+ command: addSource,
+ );
+
+ if (!result.success) {
+ throw AmberCtlException('"add_src" failed, aborting.', result);
+ } else {
+ stdout.writeln('Successfully added an update'
+ ' source on port $port with name $uuid.');
+ return uuid;
+ }
+ }
+
+ /// Adds a package with the given [packageName] to the device.
+ Future<void> addPackage(String packageName) async {
+ stdout.writeln('Adding $packageName...');
+ final List<String> updateCommand = <String>[
+ 'pkgctl',
+ 'resolve',
+ 'fuchsia-pkg://fuchsia.com/$packageName',
+ ];
+
+ final OperationResult result = await _kSsh.runCommand(
+ _targetIp,
+ identityFilePath: _identityFile,
+ command: updateCommand,
+ );
+
+ if (!result.success) {
+ throw AmberCtlException(
+ '${updateCommand.join(' ')} failed, aborting.', result);
+ }
+ }
+
+ Future<String> _getLocalIp(String targetIp) async {
+ final OperationResult result = await _kSsh.runCommand(targetIp,
+ identityFilePath: _identityFile,
+ command: <String>[r'echo $SSH_CONNECTION']);
+
+ if (!result.success) {
+ throw AmberCtlException('Failed to get local address, aborting.', result);
+ } else {
+ return result.info.split(' ')[0].replaceAll('%', '%25');
+ }
+ }
+}
+
+/// Wraps exceptions thrown by amberctl utility.
+@immutable
+class AmberCtlException implements Exception {
+ /// Creates a new [AmberCtlException] using the specified [cause] and [result].
+ const AmberCtlException(this.cause, this.result);
+
+ /// Represents the human-readable cause for the amberctl error.
+ final String cause;
+
+ /// Contains the result of the executed target command.
+ final OperationResult result;
+
+ @override
+ String toString() =>
+ '$runtimeType, cause: "$cause", underlying exception: $result.';
+}
diff --git a/packages/fuchsia_ctl/lib/src/command_line.dart b/packages/fuchsia_ctl/lib/src/command_line.dart
new file mode 100644
index 0000000..2873b75
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/command_line.dart
@@ -0,0 +1,81 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:meta/meta.dart';
+import 'package:process/process.dart';
+
+/// Class for common actions with processes on the command line.
+@immutable
+class CommandLine {
+ /// Create a new instance of [CommandLine].
+ const CommandLine(
+ {this.processManager = const LocalProcessManager(),
+ @visibleForTesting this.stdoutValue,
+ @visibleForTesting this.stderrValue});
+
+ /// The underlying [ProcessManager] to use for running on the current shell.
+ final ProcessManager processManager;
+
+ /// Mock value for use in tests.
+ final Stdout stdoutValue;
+
+ /// Mock value for use in tests.
+ final Stdout stderrValue;
+
+ /// Current shells stdout to use.
+ Stdout get shellStdout => stdoutValue ?? stdout;
+
+ /// Current shells stderr to use.
+ Stdout get shellStderr => stderrValue ?? stderr;
+
+ /// Run [command] and handle its stdio. Once [command] is complete, it will
+ /// output its stdio to the console.
+ ///
+ /// Use this for tasks where stdio does not need to be monitored.
+ ///
+ /// Throw [CommandLineException] if [command] returns a non-0 exit code.
+ Future<void> run(List<String> command) async {
+ shellStdout.writeln(command.join(' '));
+ final ProcessResult process = await processManager.run(command);
+ shellStdout.writeln(process.stdout);
+ shellStderr.writeln(process.stderr);
+
+ if (process.exitCode != 0) {
+ throw CommandLineException('${command.first} did not return exit code 0');
+ }
+ }
+
+ /// Start [command] and handle its stdio by streaming it to the existing
+ /// stdio. While [command] is running, its stdio is streamed to the shell.
+ ///
+ /// Use this for long running tasks where stdio should be monitored.
+ ///
+ /// Throw [CommandLineException] if [command] returns a non-0 exit code.
+ Future<Process> start(List<String> command) async {
+ shellStdout.writeln(command.join(' '));
+ final Process process = await processManager.start(command);
+ shellStdout.addStream(process.stdout);
+ shellStderr.addStream(process.stderr);
+
+ if (await process.exitCode != 0) {
+ throw CommandLineException('${command.first} did not return exit code 0');
+ }
+
+ return process;
+ }
+}
+
+/// Wraps exceptions thrown by [CommandLine].
+class CommandLineException implements Exception {
+ /// Creates a new [CommandLineException].
+ const CommandLineException(this.message);
+
+ /// The user-facing message to display.
+ final String message;
+
+ @override
+ String toString() => message;
+}
diff --git a/packages/fuchsia_ctl/lib/src/dev_finder.dart b/packages/fuchsia_ctl/lib/src/dev_finder.dart
new file mode 100644
index 0000000..ac91213
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/dev_finder.dart
@@ -0,0 +1,136 @@
+// 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 'dart:io';
+
+import 'package:meta/meta.dart';
+import 'package:process/process.dart';
+
+/// A wrapper for the Fuchsia SDK `device-finder` tool.
+@immutable
+class DevFinder {
+ /// Creates a new wrapper for the `device-finder` tool.
+ ///
+ /// All parameters must not be null.
+ const DevFinder(
+ this.devFinderPath, {
+ this.processManager = const LocalProcessManager(),
+ }) : assert(devFinderPath != null),
+ assert(processManager != null);
+
+ /// The path to the Fuchsia SDK `device-finder` tool on disk.
+ final String devFinderPath;
+
+ /// The [ProcessManager] to use for launching the `device-finder` tool.
+ final ProcessManager processManager;
+
+ Future<String> _runDevFinderWithRetries(
+ String deviceName,
+ int numTries,
+ int sleepDelay, {
+ bool local = false,
+ @required bool nullOk,
+ }) async {
+ assert(numTries != null);
+ assert(sleepDelay != null);
+ assert(local != null);
+ assert(nullOk != null);
+
+ if (deviceName == null) {
+ stderr.writeln('Warning: device name not specified; if '
+ 'multiple devices are attached you may not get the right one.');
+ }
+ final List<String> command = <String>[
+ devFinderPath,
+ if (deviceName != null) 'resolve' else 'list',
+ '-device-limit', '1', //
+ if (local) '-local',
+ if (deviceName != null) deviceName,
+ ];
+
+ for (int i = 0; i < numTries; i++) {
+ final ProcessResult result = await processManager.run(command);
+ if (result.exitCode == 0) {
+ return result.stdout.toString().trim();
+ }
+ await Future<void>.delayed(Duration(seconds: sleepDelay));
+ }
+ if (!nullOk) {
+ throw DevFinderException(
+ 'Failed to get ${local ? 'local' : 'target'} IP for $deviceName');
+ }
+ return null;
+ }
+
+ /// Gets the target address for the specified `deviceName`.
+ ///
+ /// If `deviceName` is null, will attempt to get the target address of the
+ /// first discoverable device.
+ ///
+ /// The `numTries` parameter must not be null, and specifies how many
+ /// times to retry finding the device. This is useful e.g. after paving a
+ /// device and waiting for it to become available. The `sleepDelay` also
+ /// must not be null, and specifies the number of seconds to wait between
+ /// retries.
+ ///
+ /// The `nullOk` parameter must not be null. If true, this method will
+ /// return null if it cannot find a device; otherwise, it will throw. The
+ /// default value is false.
+ Future<String> getTargetAddress(
+ String deviceName, {
+ int numTries = 75,
+ int sleepDelay = 4,
+ bool nullOk = false,
+ }) {
+ return _runDevFinderWithRetries(
+ deviceName,
+ numTries,
+ sleepDelay,
+ nullOk: nullOk,
+ local: false,
+ );
+ }
+
+ /// Gets the local interface address for the specified `deviceName`.
+ ///
+ /// If `deviceName` is null, will attempt to get the target address of the
+ /// first discoverable device.
+ ///
+ /// The `numTries` parameter must not be null, and specifies how many
+ /// times to retry finding the device. This is useful e.g. after paving a
+ /// device and waiting for it to become available. The `sleepDelay` also
+ /// must not be null, and specifies the number of seconds to wait between
+ /// retries.
+ ///
+ /// The `nullOk` parameter must not be null. If true, this method will
+ /// return null if it cannot find a device; otherwise, it will throw. The
+ /// default value is false.
+ Future<String> getLocalAddress(
+ String deviceName, {
+ int numTries = 30,
+ int sleepDelay = 2,
+ bool nullOk = false,
+ }) {
+ return _runDevFinderWithRetries(
+ deviceName,
+ numTries,
+ sleepDelay,
+ nullOk: nullOk,
+ local: true,
+ );
+ }
+}
+
+/// The exception thrown when a [DevFinder] lookup fails.
+class DevFinderException implements Exception {
+ /// Creates a new [DevFinderException], such as when device-finder fails to find
+ /// a device.
+ const DevFinderException(this.message);
+
+ /// The user-facing message to display.
+ final String message;
+
+ @override
+ String toString() => message;
+}
diff --git a/packages/fuchsia_ctl/lib/src/emulator.dart b/packages/fuchsia_ctl/lib/src/emulator.dart
new file mode 100644
index 0000000..1e23647
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/emulator.dart
@@ -0,0 +1,190 @@
+// 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:file/file.dart';
+import 'package:file/local.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as path;
+
+import 'command_line.dart';
+import 'operation_result.dart';
+import 'ssh_key_manager.dart';
+
+/// A wrapper for running Fuchsia images on the Android Emulator (AEMU).
+class Emulator {
+ /// Creates a new wrapper for the `emu` tool.
+ Emulator({
+ @required this.aemuPath,
+ @required this.fuchsiaImagePath,
+ @required this.fuchsiaSdkPath,
+ this.fs = const LocalFileSystem(),
+ this.cli = const CommandLine(),
+ @required this.qemuKernelPath,
+ @required this.sshKeyManager,
+ @required this.zbiPath,
+ }) : assert(cli != null),
+ assert(fs != null);
+
+ /// The path to the AEMU executable on disk.
+ final String aemuPath;
+
+ /// Fuchsia image to load into the emulator.
+ final String fuchsiaImagePath;
+
+ /// The path to the Fuchsia SDK that contains the tools `fvm` and `zbi`.
+ final String fuchsiaSdkPath;
+
+ /// The QEMU kernel image to use. This is only bundled in Fuchsia QEMU images.
+ final String qemuKernelPath;
+
+ /// The Fuchsia bootloader image.
+ final String zbiPath;
+
+ /// Location of `fvm` in [fuchsiaSdkPath].
+ @visibleForTesting
+ final String fvmToolPath = 'sdk/tools/fvm';
+
+ /// Location of `zbi` in [fuchsiaSdkPath].
+ @visibleForTesting
+ final String zbiToolPath = 'sdk/tools/zbi';
+
+ /// Default AEMU window size to be launched.
+ @visibleForTesting
+ final String defaultWindowSize = '1280x800';
+
+ /// Flag to pass to AEMU to run in headless mode.
+ @visibleForTesting
+ final String aemuHeadlessFlag = '-no-window';
+
+ /// [SshKeyManager] for creating `authorized_keys` to access emulator.
+ final SshKeyManager sshKeyManager;
+
+ /// The [FileSystem] to use when running the `emu` tool.
+ final FileSystem fs;
+
+ /// The [CommandLine] wrapper for interacting with the current shell.
+ final CommandLine cli;
+
+ /// FVM extended image of [fuchsiaImagePath] for running on FEMU.
+ @visibleForTesting
+ String fvmImagePath;
+
+ /// [zbiPath] that is accessible with SSH using [sshPath] keys.
+ @visibleForTesting
+ String signedZbiPath;
+
+ /// Update given Fuchsia assets to make them compatible with FEMU.
+ ///
+ /// 1. Ensure required assets exist.
+ /// 2. Create FVM image for running with FEMU.
+ /// 3. Sign boot image for host access to the guest FEMU instance.
+ Future<void> prepareEnvironment() async {
+ assert(fs.isFileSync(fuchsiaImagePath));
+ assert(fs.isFileSync(zbiPath));
+ assert(fs.isFileSync(qemuKernelPath));
+
+ final String tmpPath = fs.systemTempDirectory.createTempSync().path;
+ fvmImagePath = '$tmpPath/fvm.blk';
+ signedZbiPath = '$tmpPath/fuchsia-ssh.zbi';
+
+ await _prepareFvmImage(fuchsiaImagePath, fvmImagePath);
+ await _signBootImage(zbiPath, signedZbiPath);
+ }
+
+ /// Double the size of [fuchsiaImagePath] to make space for the emulator
+ /// to write back to it.
+ Future<void> _prepareFvmImage(String fuchsiaImagePath, String fvmPath,
+ {String fvmExecutable}) async {
+ fvmExecutable ??= path.join(fuchsiaSdkPath, fvmToolPath);
+
+ await cli.run(<String>['cp', fuchsiaImagePath, fvmPath]);
+
+ /// [fvmTool] and FEMU need write access to [fvmPath].
+ await cli.run(<String>['chmod', 'u+w', fvmPath]);
+
+ // Calculate new size by doubling the current size
+ final File fvmFile = fs.file(fvmPath)..createSync();
+ final int newSize = fvmFile.lengthSync() * 2;
+
+ await cli.run(
+ <String>[fvmExecutable, fvmPath, 'extend', '--length', '$newSize']);
+ }
+
+ /// Signed [zbiPath] using [zbiExecutable] with [publicKeyPath] to
+ /// create a bootloader image that is accessible from the host.
+ Future<void> _signBootImage(String zbiPath, String signedZbiPath,
+ {String zbiExecutable}) async {
+ zbiExecutable ??= path.join(fuchsiaSdkPath, zbiToolPath);
+
+ await sshKeyManager.createKeys();
+
+ /// Ensure `zbi` is able to find the ssh keys by giving the full path.
+ final File authorizedKeysAbsolute =
+ fs.file('.ssh/authorized_keys').absolute;
+
+ final List<String> zbiCommand = <String>[
+ zbiExecutable,
+ '--compressed=zstd',
+ '-o',
+ signedZbiPath,
+ zbiPath,
+ '-e',
+ 'data/ssh/authorized_keys=${authorizedKeysAbsolute.path}'
+ ];
+ await cli.run(zbiCommand);
+ }
+
+ /// Launch AEMU with [fvmImagePath], [signedZbiPath], and [qemuKernelPath].
+ ///
+ /// [prepareEnvironment] must have been called before starting the emulator.
+ ///
+ /// If [headless] is true, AEMU will run without a graphical window. Infra
+ /// will run AEMU in headless mode.
+ ///
+ /// [windowSize] is what AEMU will set its window size to. Defaults to
+ /// [defaultWindowSize]. Expected to be in the format of "WIDTHxHEIGHT".
+ Future<OperationResult> start(
+ {bool headless = false, String windowSize}) async {
+ assert(fvmImagePath != null && fs.isFileSync(fvmImagePath));
+ assert(signedZbiPath != null && fs.isFileSync(signedZbiPath));
+
+ final List<String> aemuCommand = <String>[
+ aemuPath,
+ '-feature',
+ 'VirtioInput,RefCountPipe,KVM,GLDirectMem,Vulkan',
+ '-window-size',
+ windowSize ?? defaultWindowSize,
+ '-gpu',
+ 'swiftshader_indirect',
+ if (headless) aemuHeadlessFlag,
+
+ /// Anything after -fuchsia flag will be passed to QEMU
+ '-fuchsia',
+ '-kernel', qemuKernelPath,
+ '-initrd', signedZbiPath,
+ '-m', '2048',
+ '-serial', 'stdio',
+ '-vga', 'none',
+ '-device', 'virtio-keyboard-pci',
+ '-device', 'virtio_input_multi_touch_pci_1',
+ '-smp', '4,threads=2',
+ '-machine', 'q35',
+ '-device', 'isa-debug-exit,iobase=0xf4,iosize=0x04',
+ // TODO(chillers): Add hardware acceleration option to configure this.
+ '-enable-kvm',
+ '-cpu', 'host,migratable=no,+invtsc',
+ '-netdev', 'type=tap,ifname=qemu,script=no,downscript=no,id=net0',
+ '-device', 'e1000,netdev=net0,mac=52:54:00:63:5e:7a',
+ '-drive', 'file=$fvmImagePath,format=raw,if=none,id=vdisk',
+ '-device', 'virtio-blk-pci,drive=vdisk',
+ '-append',
+ // TODO(chillers): Generate entropy mixin.
+ '\'TERM=xterm-256color kernel.serial=legacy kernel.entropy-mixin=660486b6b20b4ace3fb5c81b0002abf5271289185c6a5620707606c55b377562 kernel.halt-on-panic=true\'',
+ ];
+
+ await cli.start(aemuCommand);
+
+ return OperationResult.success();
+ }
+}
diff --git a/packages/fuchsia_ctl/lib/src/image_paver.dart b/packages/fuchsia_ctl/lib/src/image_paver.dart
new file mode 100644
index 0000000..73eabf2
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/image_paver.dart
@@ -0,0 +1,139 @@
+// 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 'dart:convert';
+import 'dart:io';
+
+import 'package:file/file.dart';
+import 'package:file/local.dart';
+import 'package:meta/meta.dart';
+import 'package:process/process.dart';
+import 'package:uuid/uuid.dart';
+
+import 'operation_result.dart';
+import 'ssh_key_manager.dart';
+import 'tar.dart';
+
+/// Paves a prebuilt system image to a Fuchsia device.
+///
+/// The Fuchsia device must be in zedboot mode.
+@immutable
+class ImagePaver {
+ /// Creates a new image paver.
+ ///
+ /// All properties must not be null.
+ const ImagePaver({
+ this.processManager = const LocalProcessManager(),
+ this.fs = const LocalFileSystem(),
+ this.tar = const SystemTar(processManager: LocalProcessManager()),
+ this.sshKeyManagerProvider = SystemSshKeyManager.defaultProvider,
+ }) : assert(processManager != null),
+ assert(fs != null),
+ assert(tar != null);
+
+ /// The [ProcessManager] used to launch the boot server, `tar`,
+ /// and `ssh-keygen`.
+ final ProcessManager processManager;
+
+ /// The default pave timeout as [Duration] in milliseconds.
+ static const Duration defaultPaveTimeoutMs =
+ Duration(milliseconds: 5 * 60 * 1000);
+
+ /// The [FileSystem] implementation used to
+ final FileSystem fs;
+
+ /// The implementation to use for untarring system images.
+ final Tar tar;
+
+ /// The implementation to use for creating SSH keys.
+ final SshKeyManagerProvider sshKeyManagerProvider;
+
+ /// Paves an image (in .tgz format) to the specified device.
+ ///
+ /// The `imageTgzPath` must not be null. If `deviceName` is null, the
+ /// first discoverable device will be used.
+ Future<OperationResult> pave(
+ String imageTgzPath,
+ String deviceName, {
+ String publicKeyPath,
+ bool verbose = true,
+ Duration timeoutMs = defaultPaveTimeoutMs,
+ }) async {
+ assert(imageTgzPath != null);
+ if (deviceName == null) {
+ stderr.writeln('Warning: No device name specified. '
+ 'If multiple devices are attached, this may result in paving '
+ 'an unexpected device.');
+ }
+ final SshKeyManager sshKeyManager = sshKeyManagerProvider(
+ processManager: processManager,
+ publicKeyPath: publicKeyPath,
+ fs: fs,
+ );
+ final String uuid = Uuid().v4();
+ final Directory imageDirectory = fs.directory('image_$uuid');
+ if (verbose) {
+ stdout.writeln('Using ${imageDirectory.path} as temp path.');
+ }
+ await imageDirectory.create();
+ final OperationResult untarResult = await tar.untar(
+ imageTgzPath,
+ imageDirectory.path,
+ );
+
+ if (!untarResult.success) {
+ if (verbose) {
+ stderr.writeln('Unpacking image $imageTgzPath failed.');
+ }
+ imageDirectory.deleteSync(recursive: true);
+ return untarResult;
+ }
+
+ final OperationResult sshResult = await sshKeyManager.createKeys();
+ if (!sshResult.success) {
+ if (verbose) {
+ stderr.writeln('Creating SSH Keys failed.');
+ }
+ imageDirectory.deleteSync(recursive: true);
+ return sshResult;
+ }
+ final Process paveProcess = await processManager.start(
+ <String>[
+ '${imageDirectory.path}/pave.sh',
+ '--fail-fast',
+ '-1', // pave once and exit
+ '--allow-zedboot-version-mismatch',
+ if (deviceName != null) ...<String>['-n', deviceName],
+ '--authorized-keys', '.ssh/authorized_keys',
+ ],
+ ).timeout(timeoutMs);
+ final StringBuffer paveStdout = StringBuffer();
+ final StringBuffer paveStderr = StringBuffer();
+ paveProcess.stdout.transform(utf8.decoder).forEach((String s) {
+ if (verbose) {
+ stdout.write(s);
+ }
+ paveStdout.write(s);
+ });
+ paveProcess.stderr.transform(utf8.decoder).forEach((String s) {
+ if (verbose) {
+ stderr.write(s);
+ }
+ paveStderr.write(s);
+ });
+ final int exitCode = await paveProcess.exitCode;
+ await stdout.flush();
+ await stderr.flush();
+ imageDirectory.deleteSync(recursive: true);
+
+ return OperationResult.fromProcessResult(
+ ProcessResult(
+ paveProcess.pid,
+ exitCode,
+ paveStdout.toString(),
+ paveStderr.toString(),
+ ),
+ );
+ }
+}
diff --git a/packages/fuchsia_ctl/lib/src/logger.dart b/packages/fuchsia_ctl/lib/src/logger.dart
new file mode 100644
index 0000000..5f908b3
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/logger.dart
@@ -0,0 +1,131 @@
+import 'dart:io';
+
+/// Defines the available log levels.
+class LogLevel {
+ const LogLevel._(this._level, this.name);
+
+ final int _level;
+
+ /// String name for the log level.
+ final String name;
+
+ /// LogLevel for messages instended for debugging.
+ static const LogLevel debug = LogLevel._(0, 'DEBUG');
+
+ /// LogLevel for messages instended to provide information about the
+ /// execution.
+ static const LogLevel info = LogLevel._(1, 'INFO');
+
+ /// LogLevel for messages instended to flag potential problems.
+ static const LogLevel warning = LogLevel._(2, 'WARN');
+
+ /// LogLevel for errors in the execution.
+ static const LogLevel error = LogLevel._(3, 'ERROR');
+}
+
+/// Abstract class for loggers.
+abstract class Logger {
+ /// Processes a debug message.
+ void debug(Object message);
+
+ /// Processes an info message.
+ void info(Object message);
+
+ /// Processes a warning message.
+ void warning(Object message);
+
+ /// Processes an error message.
+ void error(Object message);
+}
+
+/// Logger to print message to standard output.
+class PrintLogger implements Logger {
+ /// Creates a logger instance to print messages to standard output.
+ PrintLogger({
+ IOSink out,
+ bool prependLogData,
+ this.level = LogLevel.info,
+ }) : out = out ?? stdout,
+ prependLogData = prependLogData ?? true;
+
+ /// The [IOSink] to print to.
+ final IOSink out;
+
+ /// Available log levels.
+ final LogLevel level;
+
+ /// Wether to prepend datetime and log level or not.
+ final bool prependLogData;
+
+ /// Stdout buffer.
+ final StringBuffer stdoutBuffer = StringBuffer();
+
+ /// Stderr buffer.
+ final StringBuffer stderrBuffer = StringBuffer();
+
+ /// Returns all the content logged as info, debug and warning without the
+ /// datetime and log level prepended to lines.
+ String outputLog() {
+ return stdoutBuffer.toString();
+ }
+
+ /// Returns all the content logged error without the
+ /// datetime and log level prepended to lines.
+ String errorLog() {
+ return stderrBuffer.toString();
+ }
+
+ @override
+ void debug(Object message) {
+ _log(LogLevel.debug, message);
+ stdoutBuffer.writeln(message);
+ }
+
+ @override
+ void info(Object message) {
+ _log(LogLevel.info, message);
+ stdoutBuffer.writeln(message);
+ }
+
+ @override
+ void warning(Object message) {
+ _log(LogLevel.warning, message);
+ stdoutBuffer.writeln(message);
+ }
+
+ @override
+ void error(Object message) {
+ _log(LogLevel.error, message);
+ stderrBuffer.writeln(message);
+ }
+
+ void _log(LogLevel level, Object message) {
+ if (prependLogData) {
+ if (level._level >= this.level._level) {
+ out.writeln(toLogString('$message', level: level));
+ }
+ } else {
+ out.writeln('$message');
+ }
+ }
+
+ /// Flushes the IOSink to ensure all the data is written. This is specially
+ /// useful when writing to a file.
+ Future<void> flush() async {
+ await out.flush();
+ }
+}
+
+/// Transforms a [message] with [level] to a string that contains the DateTime,
+/// level and message.
+String toLogString(String message, {LogLevel level}) {
+ final StringBuffer buffer = StringBuffer();
+ buffer.write(DateTime.now().toUtc().toIso8601String());
+ buffer.write(': ');
+ if (level != null) {
+ buffer.write(level.name);
+ buffer.write(' ');
+ }
+ buffer.write(message);
+ return buffer.toString();
+}
diff --git a/packages/fuchsia_ctl/lib/src/operation_result.dart b/packages/fuchsia_ctl/lib/src/operation_result.dart
new file mode 100644
index 0000000..b4c76cb
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/operation_result.dart
@@ -0,0 +1,66 @@
+// 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 'dart:io';
+
+import 'package:meta/meta.dart';
+
+/// The result of running a command or operation.
+///
+/// This loosely corresponds to a [ProcessResult], but is not necessarily the
+/// result of running a process.
+@immutable
+class OperationResult {
+ const OperationResult._(
+ this.success, {
+ this.info = '',
+ this.error = '',
+ });
+
+ /// A successful operation result with a non-null but potentially empty info
+ /// message, and an empty error message.
+ factory OperationResult.success({
+ String info = '',
+ }) {
+ assert(info != null);
+ return OperationResult._(true, info: info, error: '');
+ }
+
+ /// A failing operation result with a non-null but potentially empty error,
+ /// and a non-null but potentially empty info.
+ factory OperationResult.error(
+ String error, {
+ String info = '',
+ }) {
+ assert(error != null);
+ assert(info != null);
+ return OperationResult._(false, info: info, error: error);
+ }
+
+ /// Creates an [OperationResult] from a [ProcessResult].
+ factory OperationResult.fromProcessResult(
+ ProcessResult result, {
+ int expectedExitCode = 0,
+ }) {
+ assert(expectedExitCode != null);
+ return OperationResult._(
+ result.exitCode == expectedExitCode,
+ info: result.stdout.toString(),
+ error: result.stderr.toString(),
+ );
+ }
+
+ /// Whether the result was successful or not. Not null.
+ final bool success;
+
+ /// Information from the result, e.g. the stdout of a process.
+ final String info;
+
+ /// Error information from the result, e.g. the stderr of a process.
+ final String error;
+
+ @override
+ String toString() =>
+ '$runtimeType{${success ? 'success' : 'failure'}, info: "$info", error: "$error"}';
+}
diff --git a/packages/fuchsia_ctl/lib/src/package_server.dart b/packages/fuchsia_ctl/lib/src/package_server.dart
new file mode 100644
index 0000000..752b34c
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/package_server.dart
@@ -0,0 +1,151 @@
+// 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 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:file/file.dart';
+import 'package:file/local.dart';
+import 'package:process/process.dart';
+import 'package:path/path.dart' as path;
+import 'package:uuid/uuid.dart';
+
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+
+/// A wrapper around the Fuchsia SDK `pm` tool.
+class PackageServer {
+ /// Creates a new package server.
+ PackageServer(
+ this.pmPath, {
+ this.processManager = const LocalProcessManager(),
+ this.fileSystem = const LocalFileSystem(),
+ }) : assert(pmPath != null),
+ assert(processManager != null),
+ assert(fileSystem != null);
+
+ /// The path on the file system to the `pm` executable.
+ final String pmPath;
+
+ /// The process manager to use for launching `pm`.
+ final ProcessManager processManager;
+
+ /// The file sytem for the package server.
+ final FileSystem fileSystem;
+
+ Process _pmServerProcess;
+
+ /// Path to the port file generated by `pm`.
+ String portPath;
+
+ /// The port the server is listening on, if the server is running.
+ ///
+ /// Throws a [StateError] if accessed when the server is not running.
+ int get serverPort {
+ if (_pmServerProcess == null) {
+ throw StateError('Attempted to get port before starting server.');
+ }
+ return _serverPort;
+ }
+
+ int _serverPort;
+
+ /// Is the server up?
+ bool get serving {
+ return _pmServerProcess != null;
+ }
+
+ /// Creates a new local repository and associated key material.
+ ///
+ /// Corresponds to `pm newrepo`.
+ Future<OperationResult> newRepo(String repo) async {
+ return OperationResult.fromProcessResult(
+ await processManager.run(
+ <String>[
+ pmPath,
+ 'newrepo',
+ '-repo', repo, //
+ ],
+ ),
+ );
+ }
+
+ /// Publishes an archive package for use on a device with the specified
+ /// .far files.
+ Future<OperationResult> publishRepo(String repo, String farFile) async {
+ return OperationResult.fromProcessResult(
+ await processManager.run(
+ <String>[
+ pmPath,
+ 'publish',
+ '-a',
+ '-repo', repo, //
+ '-f', farFile,
+ ],
+ ),
+ );
+ }
+
+ /// Starts a server for the specified repo path and port.
+ ///
+ /// Use port 0 to have the server choose a port. The acutal port used
+ /// will be avialalbe in the [serverPort] property after this method
+ /// returns. The stdout and stderr of the server will be printed to [stdout]
+ /// and [stderr], respectively.
+ Future<void> serveRepo(
+ String repo, {
+ String address = '',
+ int port = 0,
+ String portFilePath,
+ }) async {
+ assert(repo != null);
+ assert(port != null);
+
+ final String uuid = Uuid().v4();
+ portPath = portFilePath ??
+ path.join(fileSystem.systemTempDirectory.path, '${uuid}_port.txt');
+ final List<String> pmCommand = <String>[
+ pmPath,
+ 'serve',
+ '-repo',
+ repo,
+ '-l',
+ '$address:$port',
+ '-f',
+ portPath,
+ ];
+ stdout.writeln('Running ${pmCommand.join(' ')}');
+ _pmServerProcess = await processManager.start(pmCommand);
+ await Future<void>.delayed(const Duration(seconds: 5), () async {
+ final String portString = await fileSystem.file(portPath).readAsString();
+ _serverPort = int.parse(portString);
+ });
+ _pmServerProcess.stdout
+ .transform(utf8.decoder)
+ .transform(const LineSplitter())
+ .listen(stdout.writeln);
+ _pmServerProcess.stderr
+ .transform(utf8.decoder)
+ .transform(const LineSplitter())
+ .listen(stderr.writeln);
+ }
+
+ /// Closes a running server.
+ ///
+ /// Calling this before calling [serveRepo] will result in a [StateError].
+ Future<OperationResult> close() async {
+ if (_pmServerProcess == null) {
+ throw StateError('Must call serveRepo before calling close.');
+ }
+ await fileSystem.file(portPath).delete();
+ _pmServerProcess.kill();
+ final int exitCode = await _pmServerProcess.exitCode;
+ _pmServerProcess = null;
+ if (exitCode == 0) {
+ return OperationResult.success();
+ }
+ return OperationResult.error(
+ 'The "pm" executable exited with non-zero exit code.');
+ }
+}
diff --git a/packages/fuchsia_ctl/lib/src/ssh_client.dart b/packages/fuchsia_ctl/lib/src/ssh_client.dart
new file mode 100644
index 0000000..35d20e4
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/ssh_client.dart
@@ -0,0 +1,163 @@
+// 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 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:file/file.dart';
+import 'package:file/local.dart';
+import 'package:fuchsia_ctl/src/logger.dart';
+import 'package:meta/meta.dart';
+import 'package:process/process.dart';
+
+import 'operation_result.dart';
+
+/// A client for running SSH based commands on a Fuchsia device.
+@immutable
+class SshClient {
+ /// Creates a new SSH client.
+ ///
+ /// Relies on `ssh` being present in the $PATH.
+ ///
+ /// The `processManager` must not be null.
+ const SshClient({
+ this.processManager = const LocalProcessManager(),
+ }) : assert(processManager != null);
+
+ /// The [ProcessManager] to use for spawning `ssh`.
+ final ProcessManager processManager;
+
+ /// The default ssh timeout as [Duration] in milliseconds.
+ static const Duration defaultSshTimeoutMs =
+ Duration(milliseconds: 5 * 60 * 1000);
+
+ /// Creates a list of arguments to pass to ssh.
+ ///
+ /// This method is not intended for use outside of this library, except for
+ /// in unit tests.
+ @visibleForTesting
+ List<String> getSshArguments({
+ String identityFilePath,
+ String targetIp,
+ List<String> command = const <String>[],
+ }) {
+ assert(command != null);
+ return <String>[
+ 'ssh',
+ '-o', 'CheckHostIP=no', //
+ '-o', 'StrictHostKeyChecking=no',
+ '-o', 'ForwardAgent=no',
+ '-o', 'ForwardX11=no',
+ '-o', 'GSSAPIDelegateCredentials=no',
+ '-o', 'UserKnownHostsFile=/dev/null',
+ '-o', 'User=fuchsia',
+ '-o', 'IdentitiesOnly=yes',
+ '-o', 'IdentityFile=$identityFilePath',
+ '-o', 'ControlPersist=yes',
+ '-o', 'ControlMaster=auto',
+ '-o', 'ControlPath=/tmp/fuchsia--%r@%h:%p',
+ '-o', 'ServerAliveInterval=1',
+ '-o', 'ServerAliveCountMax=10',
+ '-o', 'LogLevel=ERROR',
+ targetIp,
+ command.join(' '),
+ ];
+ }
+
+ /// Creates an interactive SSH session.
+ Future<OperationResult> interactive(
+ String targetIp, {
+ @required String identityFilePath,
+ }) async {
+ final Process ssh = await processManager.start(getSshArguments(
+ targetIp: targetIp,
+ identityFilePath: identityFilePath,
+ ));
+ ssh.stdout.transform(utf8.decoder).listen(stdout.writeln);
+ ssh.stderr.transform(utf8.decoder).listen(stderr.writeln);
+ stdin.pipe(ssh.stdin);
+
+ final int exitCode = await ssh.exitCode;
+ if (exitCode == 0) {
+ return OperationResult.success();
+ }
+ return OperationResult.error('ssh exited with code $exitCode');
+ }
+
+ /// Runs an SSH command on the specified target IP.
+ ///
+ /// A target IP can be obtained from a device node name using the
+ /// [DevFinder] class.
+ ///
+ /// All arguments must not be null.
+ Future<OperationResult> runCommand(
+ String targetIp, {
+ @required String identityFilePath,
+ @required List<String> command,
+ Duration timeoutMs = defaultSshTimeoutMs,
+ String logFilePath,
+ FileSystem fs,
+ }) async {
+ assert(targetIp != null);
+ assert(identityFilePath != null);
+ assert(command != null);
+
+ final bool logToFile = !(logFilePath == null || logFilePath.isEmpty);
+ final FileSystem fileSystem = fs ?? const LocalFileSystem();
+ PrintLogger logger;
+
+ // If no file is passed to this method we create a memoryfile to keep to
+ // return the stdout in OperationResult.
+ if (logToFile) {
+ fileSystem.file(logFilePath).existsSync() ??
+ fileSystem.file(logFilePath).deleteSync();
+ fileSystem.file(logFilePath).createSync();
+ final IOSink data = fileSystem.file(logFilePath).openWrite();
+ logger = PrintLogger(out: data);
+ } else {
+ logger = PrintLogger();
+ }
+
+ final Process process = await processManager.start(
+ getSshArguments(
+ identityFilePath: identityFilePath,
+ targetIp: targetIp,
+ command: command,
+ ),
+ );
+ final StreamSubscription<String> stdoutSubscription = process.stdout
+ .transform(utf8.decoder)
+ .transform(const LineSplitter())
+ .listen((String log) {
+ logger.info(log);
+ });
+ final StreamSubscription<String> stderrSubscription = process.stderr
+ .transform(utf8.decoder)
+ .transform(const LineSplitter())
+ .listen((String log) {
+ logger.error(log);
+ });
+
+ // Wait for stdout and stderr to be fully processed because proc.exitCode
+ // may complete first.
+ await Future.wait<void>(<Future<void>>[
+ stdoutSubscription.asFuture<void>(),
+ stderrSubscription.asFuture<void>(),
+ ]);
+
+ await logger.flush();
+
+ // The streams as futures have already completed, so waiting for the
+ // potentially async stream cancellation to complete likely has no benefit.
+ stdoutSubscription.cancel();
+ stderrSubscription.cancel();
+
+ final int exitCode = await process.exitCode.timeout(timeoutMs);
+
+ return exitCode != 0
+ ? OperationResult.error('Failed', info: logger.errorLog())
+ : OperationResult.success(info: logger.outputLog());
+ }
+}
diff --git a/packages/fuchsia_ctl/lib/src/ssh_key_manager.dart b/packages/fuchsia_ctl/lib/src/ssh_key_manager.dart
new file mode 100644
index 0000000..8fa2245
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/ssh_key_manager.dart
@@ -0,0 +1,114 @@
+// 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 'dart:io' as io;
+
+import 'package:file/file.dart';
+import 'package:file/local.dart';
+import 'package:meta/meta.dart';
+import 'package:process/process.dart';
+
+import 'operation_result.dart';
+
+/// Function signature for a [SshKeyManager] provider.
+typedef SshKeyManagerProvider = SshKeyManager Function({
+ ProcessManager processManager,
+ FileSystem fs,
+ String publicKeyPath,
+});
+
+/// A wrapper for managing SSH key generation.
+///
+/// Implemented by [SystemSshKeyManager].
+abstract class SshKeyManager {
+ /// Create SSH key material suitable for paving and accessing a Fuchsia image.
+ Future<OperationResult> createKeys({
+ String destinationPath = '.ssh',
+ bool force = false,
+ });
+}
+
+/// A class that delegates creating SSH keys to the system `ssh-keygen`.
+@immutable
+class SystemSshKeyManager implements SshKeyManager {
+ /// Creates a wrapper for ssh-keygen.
+ ///
+ /// The arguments must not be null, and will be used to spawn a ssh-keygen
+ /// process and manipulate the files it creates.
+ const SystemSshKeyManager({
+ this.processManager = const LocalProcessManager(),
+ this.fs = const LocalFileSystem(),
+ this.pkeyPubPath,
+ }) : assert(processManager != null),
+ assert(fs != null);
+
+ /// Creates a static provider that returns a SystemSshKeyManager.
+ static SshKeyManager defaultProvider({
+ ProcessManager processManager,
+ FileSystem fs,
+ String publicKeyPath,
+ }) {
+ return SystemSshKeyManager(
+ processManager: processManager ?? const LocalProcessManager(),
+ fs: fs ?? const LocalFileSystem(),
+ pkeyPubPath: publicKeyPath,
+ );
+ }
+
+ /// The [ProcessManager] implementation to use when spawning ssh-keygen.
+ final ProcessManager processManager;
+
+ /// The [FileSystem] implementation to use when creating the authorized_keys
+ /// file.
+ final FileSystem fs;
+
+ /// The [String] with the path to a public key.
+ final String pkeyPubPath;
+
+ /// Populates [authorizedKeys] file with the public key in [pKeyPub].
+ Future<void> createAuthorizedKeys(File authorizedKeys, File pkeyPub) async {
+ final List<String> pkeyPubParts = pkeyPub.readAsStringSync().split(' ');
+ await authorizedKeys
+ .writeAsString('${pkeyPubParts[0]} ${pkeyPubParts[1]}\n');
+ }
+
+ @override
+ Future<OperationResult> createKeys({
+ String destinationPath = '.ssh',
+ bool force = false,
+ }) async {
+ final Directory sshDir = fs.directory(destinationPath);
+ final File authorizedKeys = sshDir.childFile('authorized_keys');
+ if (authorizedKeys.existsSync() && !force) {
+ return OperationResult.success(info: 'Using previously generated keys.');
+ }
+
+ if (sshDir.existsSync()) {
+ await sshDir.delete(recursive: true);
+ }
+
+ await sshDir.create();
+ if (pkeyPubPath != null) {
+ await createAuthorizedKeys(authorizedKeys, fs.file(pkeyPubPath));
+ return OperationResult.success(info: 'Using previously generated keys.');
+ }
+
+ final File pkey = sshDir.childFile('pkey');
+ final File pkeyPub = sshDir.childFile('pkey.pub');
+ final io.ProcessResult result = await processManager.run(
+ <String>[
+ 'ssh-keygen',
+ '-t', 'ed25519', //
+ '-f', pkey.path,
+ '-q',
+ '-N', '',
+ ],
+ );
+ if (result.exitCode != 0) {
+ return OperationResult.fromProcessResult(result);
+ }
+ await createAuthorizedKeys(authorizedKeys, pkeyPub);
+ return OperationResult.fromProcessResult(result);
+ }
+}
diff --git a/packages/fuchsia_ctl/lib/src/tar.dart b/packages/fuchsia_ctl/lib/src/tar.dart
new file mode 100644
index 0000000..0a55442
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/tar.dart
@@ -0,0 +1,60 @@
+// 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 'dart:io';
+
+import 'package:meta/meta.dart';
+import 'package:process/process.dart';
+
+import 'operation_result.dart';
+
+/// A wrapper for untarring artifacts.
+///
+/// Implemented by [SystemTar].
+abstract class Tar {
+ /// A const constructor to allow subclasses to create const constructors.
+ const Tar();
+
+ /// Untars a tar file.
+ Future<OperationResult> untar(
+ String src,
+ String destination, {
+ Duration timeoutMs,
+ });
+}
+
+/// The archive package is very slow and memory intensive. Use
+/// system tar.
+@immutable
+class SystemTar implements Tar {
+ /// Creates a new [SystemTar] using the specified [ProcessManager].
+ ///
+ /// The processManager parameter must not be null.
+ const SystemTar({
+ this.processManager = const LocalProcessManager(),
+ }) : assert(processManager != null);
+
+ /// The [ProcessManager] impleemntation to use when spawning the system tar
+ /// program.
+ final ProcessManager processManager;
+
+ /// The default timeout for untar operations as [Duration] in milliseconds.
+ static const Duration defaultTarTimeoutMs =
+ Duration(milliseconds: 5 * 60 * 1000);
+
+ @override
+ Future<OperationResult> untar(
+ String src,
+ String destination, {
+ Duration timeoutMs = defaultTarTimeoutMs,
+ }) async {
+ final ProcessResult result = await processManager.run(<String>[
+ 'tar',
+ '-xf', src, //
+ '-C', destination,
+ ]).timeout(timeoutMs);
+
+ return OperationResult.fromProcessResult(result);
+ }
+}
diff --git a/packages/fuchsia_ctl/pubspec.lock b/packages/fuchsia_ctl/pubspec.lock
new file mode 100644
index 0000000..6fdecb7
--- /dev/null
+++ b/packages/fuchsia_ctl/pubspec.lock
@@ -0,0 +1,404 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ _fe_analyzer_shared:
+ dependency: transitive
+ description:
+ name: _fe_analyzer_shared
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "12.0.0"
+ analyzer:
+ dependency: transitive
+ description:
+ name: analyzer
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.40.6"
+ args:
+ dependency: "direct main"
+ description:
+ name: args
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.6.0"
+ async:
+ dependency: transitive
+ description:
+ name: async
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.4.1"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.5"
+ charcode:
+ dependency: transitive
+ description:
+ name: charcode
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.3"
+ cli_util:
+ dependency: transitive
+ description:
+ name: cli_util
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.2.0"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.14.12"
+ convert:
+ dependency: transitive
+ description:
+ name: convert
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.1"
+ coverage:
+ dependency: transitive
+ description:
+ name: coverage
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.14.2"
+ crypto:
+ dependency: transitive
+ description:
+ name: crypto
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.4"
+ file:
+ dependency: "direct main"
+ description:
+ name: file
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "5.1.0"
+ glob:
+ dependency: transitive
+ description:
+ name: glob
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ http:
+ dependency: transitive
+ description:
+ name: http
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.12.0+4"
+ http_multi_server:
+ dependency: transitive
+ description:
+ name: http_multi_server
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.2.0"
+ http_parser:
+ dependency: transitive
+ description:
+ name: http_parser
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.1.4"
+ intl:
+ dependency: transitive
+ description:
+ name: intl
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.16.1"
+ io:
+ dependency: transitive
+ description:
+ name: io
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.3.4"
+ js:
+ dependency: transitive
+ description:
+ name: js
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.6.1+1"
+ logging:
+ dependency: transitive
+ description:
+ name: logging
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.11.4"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.12.9"
+ meta:
+ dependency: "direct main"
+ description:
+ name: meta
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.3"
+ mime:
+ dependency: transitive
+ description:
+ name: mime
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.9.6+3"
+ mockito:
+ dependency: "direct dev"
+ description:
+ name: mockito
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "4.1.1"
+ node_interop:
+ dependency: transitive
+ description:
+ name: node_interop
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.3"
+ node_io:
+ dependency: transitive
+ description:
+ name: node_io
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.1+2"
+ node_preamble:
+ dependency: transitive
+ description:
+ name: node_preamble
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.4.8"
+ package_config:
+ dependency: transitive
+ description:
+ name: package_config
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.9.3"
+ package_resolver:
+ dependency: transitive
+ description:
+ name: package_resolver
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.10"
+ path:
+ dependency: "direct main"
+ description:
+ name: path
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.6.4"
+ pedantic:
+ dependency: "direct dev"
+ description:
+ name: pedantic
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.9.2"
+ platform:
+ dependency: transitive
+ description:
+ name: platform
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.2.1"
+ pool:
+ dependency: transitive
+ description:
+ name: pool
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.4.0"
+ process:
+ dependency: "direct main"
+ description:
+ name: process
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.12"
+ pub_semver:
+ dependency: transitive
+ description:
+ name: pub_semver
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.4.4"
+ retry:
+ dependency: "direct main"
+ description:
+ name: retry
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.0+1"
+ shelf:
+ dependency: transitive
+ description:
+ name: shelf
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.7.5"
+ shelf_packages_handler:
+ dependency: transitive
+ description:
+ name: shelf_packages_handler
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.4"
+ shelf_static:
+ dependency: transitive
+ description:
+ name: shelf_static
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.2.8"
+ shelf_web_socket:
+ dependency: transitive
+ description:
+ name: shelf_web_socket
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.2.3"
+ source_map_stack_trace:
+ dependency: transitive
+ description:
+ name: source_map_stack_trace
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
+ source_maps:
+ dependency: transitive
+ description:
+ name: source_maps
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.10.9"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.7.0"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.9.3"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.5"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ test:
+ dependency: "direct dev"
+ description:
+ name: test
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.15.5"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.2.18+1"
+ test_core:
+ dependency: transitive
+ description:
+ name: test_core
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.3.11+2"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.6"
+ uuid:
+ dependency: "direct main"
+ description:
+ name: uuid
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.4"
+ vm_service:
+ dependency: transitive
+ description:
+ name: vm_service
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ watcher:
+ dependency: transitive
+ description:
+ name: watcher
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.9.7+14"
+ web_socket_channel:
+ dependency: transitive
+ description:
+ name: web_socket_channel
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ webkit_inspection_protocol:
+ dependency: transitive
+ description:
+ name: webkit_inspection_protocol
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.7.4"
+ yaml:
+ dependency: transitive
+ description:
+ name: yaml
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.2.0"
+sdks:
+ dart: ">=2.7.0 <3.0.0"
diff --git a/packages/fuchsia_ctl/pubspec.yaml b/packages/fuchsia_ctl/pubspec.yaml
new file mode 100644
index 0000000..9106bb1
--- /dev/null
+++ b/packages/fuchsia_ctl/pubspec.yaml
@@ -0,0 +1,24 @@
+name: fuchsia_ctl
+description: >
+ A Dart package for paving, serving packages to, and running commands on a
+ Fuchsia Device. This package is primarily intended for use in Flutter's
+ continuous integration/testing infrastructure.
+homepage: https://github.com/flutter/packages/tree/master/packages/fuchsia_ctl
+version: 0.0.23
+
+environment:
+ sdk: ">=2.4.0 <3.0.0"
+
+dependencies:
+ args: ^1.5.2
+ file: ^5.0.10
+ meta: ^1.1.7
+ path: ^1.6.4
+ process: ^3.0.12
+ retry: ^3.0.0+1
+ uuid: ^2.0.2
+
+dev_dependencies:
+ mockito: ^4.1.1
+ pedantic: 1.9.2
+ test: ^1.15.5
diff --git a/packages/fuchsia_ctl/test/command_line_test.dart b/packages/fuchsia_ctl/test/command_line_test.dart
new file mode 100644
index 0000000..38faecd
--- /dev/null
+++ b/packages/fuchsia_ctl/test/command_line_test.dart
@@ -0,0 +1,78 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// @dart = 2.4
+import 'dart:io';
+
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:test/test.dart';
+
+import 'package:fuchsia_ctl/src/command_line.dart';
+
+import 'fakes.dart';
+
+void main() {
+ CommandLine cli;
+ MockProcessManager mockProcessManager;
+ MockStdout mockStdout;
+ MockStdout mockStderr;
+
+ group('CommandLine', () {
+ setUp(() {
+ mockStdout = MockStdout();
+ mockStderr = MockStdout();
+ mockProcessManager = MockProcessManager();
+ cli = CommandLine(
+ processManager: mockProcessManager,
+ stderrValue: mockStderr,
+ stdoutValue: mockStdout);
+ });
+
+ test('run writes to stdout', () async {
+ when(mockProcessManager.run(any))
+ .thenAnswer((_) async => ProcessResult(123, 0, 'stdout123', ''));
+
+ await cli.run(<String>['test']);
+
+ verify(mockStdout.writeln('stdout123')).called(1);
+ });
+
+ test('run writes to stderr', () async {
+ when(mockProcessManager.run(any))
+ .thenAnswer((_) async => ProcessResult(123, 0, '', 'stderrABC'));
+
+ await cli.run(<String>['test']);
+
+ verify(mockStderr.writeln('stderrABC')).called(1);
+ });
+
+ test('run throws on non-0 exit code', () async {
+ when(mockProcessManager.run(any))
+ .thenAnswer((_) async => ProcessResult(123, 1, '', 'stderrABC'));
+
+ expect(cli.run(<String>['test']), throwsA(isA<CommandLineException>()));
+ });
+
+ test('start adds streams', () async {
+ when(mockProcessManager.start(any))
+ .thenAnswer((_) async => FakeProcess(0, <String>[''], <String>['']));
+
+ await cli.start(<String>['test']);
+
+ verify(mockStdout.addStream(any)).called(1);
+ verify(mockStderr.addStream(any)).called(1);
+ });
+
+ test('start throws on non-0 exit code', () async {
+ when(mockProcessManager.start(any))
+ .thenAnswer((_) async => FakeProcess(1, <String>[''], <String>['']));
+
+ expect(cli.start(<String>['test']), throwsA(isA<CommandLineException>()));
+ });
+ });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+
+class MockStdout extends Mock implements Stdout {}
diff --git a/packages/fuchsia_ctl/test/dev_finder_test.dart b/packages/fuchsia_ctl/test/dev_finder_test.dart
new file mode 100644
index 0000000..4549a6a
--- /dev/null
+++ b/packages/fuchsia_ctl/test/dev_finder_test.dart
@@ -0,0 +1,120 @@
+// 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.
+// @dart = 2.4
+import 'dart:io';
+
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:test/test.dart';
+
+void main() {
+ const String devFinderPath = 'device-finder';
+ const String targetIp = 'target_ip';
+ const String localIp = 'local_ip';
+
+ test('finds local address with no device name', () async {
+ final MockProcessManager mockProcessManager = MockProcessManager();
+ when(mockProcessManager.run(<String>[
+ devFinderPath,
+ 'list',
+ '-device-limit',
+ '1',
+ '-local',
+ ])).thenAnswer((_) async => ProcessResult(123, 0, localIp, ''));
+
+ final DevFinder devFinder = DevFinder(
+ devFinderPath,
+ processManager: mockProcessManager,
+ );
+
+ expect(await devFinder.getLocalAddress(null), localIp);
+ });
+
+ test('finds local address with device name', () async {
+ final MockProcessManager mockProcessManager = MockProcessManager();
+ when(mockProcessManager.run(<String>[
+ devFinderPath,
+ 'resolve',
+ '-device-limit',
+ '1',
+ '-local',
+ 'devicename',
+ ])).thenAnswer((_) async => ProcessResult(123, 0, localIp, ''));
+
+ final DevFinder devFinder = DevFinder(
+ devFinderPath,
+ processManager: mockProcessManager,
+ );
+
+ expect(await devFinder.getLocalAddress('devicename'), localIp);
+ });
+
+ test('finds target address with no device name', () async {
+ final MockProcessManager mockProcessManager = MockProcessManager();
+ when(mockProcessManager.run(<String>[
+ devFinderPath,
+ 'list',
+ '-device-limit',
+ '1',
+ ])).thenAnswer((_) async => ProcessResult(123, 0, targetIp, ''));
+
+ final DevFinder devFinder = DevFinder(
+ devFinderPath,
+ processManager: mockProcessManager,
+ );
+
+ expect(await devFinder.getTargetAddress(null), targetIp);
+ });
+
+ test('finds target address with device name', () async {
+ final MockProcessManager mockProcessManager = MockProcessManager();
+ when(mockProcessManager.run(<String>[
+ devFinderPath,
+ 'resolve',
+ '-device-limit',
+ '1',
+ 'devicename',
+ ])).thenAnswer((_) async => ProcessResult(123, 0, targetIp, ''));
+
+ final DevFinder devFinder = DevFinder(
+ devFinderPath,
+ processManager: mockProcessManager,
+ );
+
+ expect(await devFinder.getTargetAddress('devicename'), targetIp);
+ });
+
+ test('retries', () async {
+ final MockProcessManager mockProcessManager = MockProcessManager();
+
+ int tries = 0;
+ when(mockProcessManager.run(<String>[
+ devFinderPath,
+ 'resolve',
+ '-device-limit',
+ '1',
+ 'devicename',
+ ])).thenAnswer((_) async {
+ tries++;
+ if (tries < 4) {
+ return ProcessResult(123, 1, '', 'Simulating device not ready yet...');
+ }
+ return ProcessResult(123, 0, targetIp, '');
+ });
+
+ final DevFinder devFinder = DevFinder(
+ devFinderPath,
+ processManager: mockProcessManager,
+ );
+
+ expect(
+ await devFinder.getTargetAddress('devicename', sleepDelay: 0),
+ targetIp,
+ );
+ expect(tries, 4);
+ });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/fuchsia_ctl/test/emulator_test.dart b/packages/fuchsia_ctl/test/emulator_test.dart
new file mode 100644
index 0000000..09f757a
--- /dev/null
+++ b/packages/fuchsia_ctl/test/emulator_test.dart
@@ -0,0 +1,114 @@
+// 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.
+// @dart = 2.4
+import 'package:file/memory.dart';
+import 'package:mockito/mockito.dart';
+import 'package:test/test.dart';
+
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+
+import 'fakes.dart';
+
+void main() {
+ const String aemuPath = '/emulator';
+ const String fuchsiaImagePath = '/fuchsia.blk';
+ const String fuchsiaSdkPath = '/fuchsia-sdk';
+ const String qemuKernelPath = '/qemu-kernel.kernel';
+ const String zbiPath = '/zircon-a.zbi';
+
+ Emulator emulator;
+ MockCommandLine mockCli;
+ MemoryFileSystem fs;
+
+ group('Emulator', () {
+ setUp(() {
+ mockCli = MockCommandLine();
+ fs = MemoryFileSystem(style: FileSystemStyle.posix);
+ fs.file(aemuPath).createSync();
+ fs.file(fuchsiaImagePath)
+ ..createSync()
+ ..writeAsString('fuchsia image content');
+ fs.file(fuchsiaSdkPath).createSync();
+ fs.file(qemuKernelPath).createSync();
+ fs.file(zbiPath).createSync();
+ emulator = Emulator(
+ aemuPath: aemuPath,
+ fs: fs,
+ fuchsiaImagePath: fuchsiaImagePath,
+ fuchsiaSdkPath: fuchsiaSdkPath,
+ cli: mockCli,
+ qemuKernelPath: qemuKernelPath,
+ sshKeyManager: MockSshKeyManager(),
+ zbiPath: zbiPath,
+ );
+ });
+
+ test('prepare environment runs as expected', () async {
+ when(mockCli.run(
+ argThat(contains('$fuchsiaSdkPath/${emulator.zbiToolPath}'))))
+ .thenAnswer((_) async {
+ fs.file(emulator.signedZbiPath).createSync();
+ });
+
+ await emulator.prepareEnvironment();
+
+ expect(fs.isFileSync(emulator.fvmImagePath), isTrue);
+ expect(fs.isFileSync(emulator.signedZbiPath), isTrue);
+ });
+
+ test('prepare environment throws on file not existing', () async {
+ fs.file(fuchsiaImagePath).deleteSync();
+
+ expect(emulator.prepareEnvironment(), throwsA(isA<AssertionError>()));
+ });
+
+ test('start throws when prepare enviornment was not called', () async {
+ expect(emulator.start(), throwsA(isA<AssertionError>()));
+
+ verifyNever(mockCli.start(any));
+ });
+
+ test('start uses default window size', () async {
+ emulator.fvmImagePath = '/fvm.blk';
+ emulator.signedZbiPath = '/fuchsia-ssh.zbi';
+ fs.file(emulator.fvmImagePath).createSync();
+ fs.file(emulator.signedZbiPath).createSync();
+
+ when(mockCli.start(any))
+ .thenAnswer((_) async => FakeProcess(0, <String>[], <String>[]));
+
+ await emulator.start();
+ verify(mockCli.start(argThat(contains(emulator.defaultWindowSize))))
+ .called(1);
+ });
+
+ test('start configures window size', () async {
+ emulator.fvmImagePath = '/fvm.blk';
+ emulator.signedZbiPath = '/fuchsia-ssh.zbi';
+ fs.file(emulator.fvmImagePath).createSync();
+ fs.file(emulator.signedZbiPath).createSync();
+
+ when(mockCli.start(any))
+ .thenAnswer((_) async => FakeProcess(0, <String>[], <String>[]));
+
+ const String customWindowSize = '50x200';
+ await emulator.start(windowSize: customWindowSize);
+ verify(mockCli.start(argThat(contains(customWindowSize)))).called(1);
+ });
+
+ test('start configures headless', () async {
+ emulator.fvmImagePath = '/fvm.blk';
+ emulator.signedZbiPath = '/fuchsia-ssh.zbi';
+ fs.file(emulator.fvmImagePath).createSync();
+ fs.file(emulator.signedZbiPath).createSync();
+
+ when(mockCli.start(any))
+ .thenAnswer((_) async => FakeProcess(0, <String>[], <String>[]));
+
+ await emulator.start(headless: true);
+ verify(mockCli.start(argThat(contains(emulator.aemuHeadlessFlag))))
+ .called(1);
+ });
+ });
+}
diff --git a/packages/fuchsia_ctl/test/fakes.dart b/packages/fuchsia_ctl/test/fakes.dart
new file mode 100644
index 0000000..19ebe5c
--- /dev/null
+++ b/packages/fuchsia_ctl/test/fakes.dart
@@ -0,0 +1,85 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:mockito/mockito.dart';
+
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+import 'package:fuchsia_ctl/src/command_line.dart';
+
+class FakeProcess implements Process {
+ FakeProcess(this._exitCode, this._stdout, this._stderr);
+
+ final int _exitCode;
+ @override
+ Future<int> get exitCode async => _exitCode;
+
+ bool _killed = false;
+ bool get killed => _killed;
+
+ @override
+ bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
+ _killed = true;
+ return true;
+ }
+
+ @override
+ int get pid => 1234;
+
+ Stream<List<int>> _streamFromString(List<String> source) =>
+ Stream<List<int>>.fromIterable(
+ source.map((String line) => utf8.encode('$line\n')));
+
+ final List<String> _stderr;
+ @override
+ Stream<List<int>> get stderr => _streamFromString(_stderr);
+
+ @override
+ IOSink get stdin => FakeIOSink();
+
+ final List<String> _stdout;
+ @override
+ Stream<List<int>> get stdout => _streamFromString(_stdout);
+}
+
+class FakeIOSink implements IOSink {
+ final Completer<void> _doneCompleter = Completer<void>.sync();
+
+ @override
+ Encoding encoding = utf8;
+
+ @override
+ void add(List<int> data) {}
+
+ @override
+ void addError(Object error, [StackTrace stackTrace]) {}
+
+ @override
+ Future<dynamic> addStream(Stream<List<int>> stream) async {}
+
+ @override
+ Future<void> close() async {}
+
+ @override
+ Future<void> get done => _doneCompleter.future;
+
+ @override
+ Future<void> flush() async {}
+
+ @override
+ void write(Object obj) {}
+
+ @override
+ void writeAll(Iterable<dynamic> objects, [String separator = '']) {}
+
+ @override
+ void writeCharCode(int charCode) {}
+
+ @override
+ void writeln([Object obj = '']) {}
+}
+
+// ignore: must_be_immutable
+class MockCommandLine extends Mock implements CommandLine {}
+
+class MockSshKeyManager extends Mock implements SshKeyManager {}
diff --git a/packages/fuchsia_ctl/test/image_paver_test.dart b/packages/fuchsia_ctl/test/image_paver_test.dart
new file mode 100644
index 0000000..41888b8
--- /dev/null
+++ b/packages/fuchsia_ctl/test/image_paver_test.dart
@@ -0,0 +1,190 @@
+// 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.
+// @dart = 2.4
+import 'dart:async';
+import 'dart:io';
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+import 'package:fuchsia_ctl/src/image_paver.dart';
+import 'package:fuchsia_ctl/src/operation_result.dart';
+import 'package:fuchsia_ctl/src/ssh_key_manager.dart';
+import 'package:fuchsia_ctl/src/tar.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:test/test.dart';
+
+import 'fakes.dart';
+
+void main() {
+ const String deviceName = 'some-name-to-use';
+ FakeSshKeyManager sshKeyManager;
+ final MemoryFileSystem fs = MemoryFileSystem(style: FileSystemStyle.posix);
+ FakeTar tar;
+ MockProcessManager processManager;
+ SshKeyManagerProvider sshKeyManagerProvider;
+
+ setUp(() {
+ processManager = MockProcessManager();
+ sshKeyManager = const FakeSshKeyManager(true);
+ sshKeyManagerProvider = ({
+ ProcessManager processManager,
+ FileSystem fs,
+ String publicKeyPath,
+ }) {
+ return sshKeyManager;
+ };
+ });
+
+ test('Tar fails', () async {
+ tar = FakeTar(false, fs);
+ when(processManager.start(any)).thenAnswer((_) async {
+ return FakeProcess(0, <String>['Good job'], <String>['']);
+ });
+
+ final ImagePaver paver = ImagePaver(
+ tar: tar,
+ sshKeyManagerProvider: sshKeyManagerProvider,
+ fs: fs,
+ processManager: processManager,
+ );
+
+ final OperationResult result = await paver.pave(
+ 'generic-x64.tgz',
+ deviceName,
+ verbose: false,
+ );
+
+ verifyNever(processManager.start(any));
+ expect(result.success, false);
+ expect(result.error, 'tar failed');
+ });
+
+ test('Ssh fails', () async {
+ sshKeyManager = const FakeSshKeyManager(false);
+ tar = FakeTar(true, fs);
+ when(processManager.start(any)).thenAnswer((_) async {
+ return FakeProcess(0, <String>['Good job'], <String>['']);
+ });
+
+ final ImagePaver paver = ImagePaver(
+ tar: tar,
+ sshKeyManagerProvider: sshKeyManagerProvider,
+ fs: fs,
+ processManager: processManager,
+ );
+
+ final OperationResult result = await paver.pave(
+ 'generic-x64.tgz',
+ deviceName,
+ verbose: false,
+ );
+
+ verifyNever(processManager.start(any));
+ expect(result.success, false);
+ expect(result.error, 'ssh failed');
+ });
+
+ test('Pave times out', () async {
+ tar = FakeTar(true, fs);
+
+ when(processManager.start(any)).thenAnswer((_) async {
+ return Future<Process>.delayed(const Duration(milliseconds: 10), () {
+ return FakeProcess(0, <String>['Good job'], <String>['']);
+ });
+ });
+
+ final ImagePaver paver = ImagePaver(
+ tar: tar,
+ sshKeyManagerProvider: sshKeyManagerProvider,
+ fs: fs,
+ processManager: processManager,
+ );
+
+ try {
+ await paver.pave(
+ 'generic-x64.tgz',
+ deviceName,
+ verbose: false,
+ timeoutMs: const Duration(milliseconds: 1),
+ );
+ } catch (e) {
+ expect(e, isA<TimeoutException>());
+ }
+ });
+
+ test('Happy path', () async {
+ tar = FakeTar(true, fs);
+ when(processManager.start(any)).thenAnswer((_) async {
+ return FakeProcess(0, <String>['Good job'], <String>['']);
+ });
+
+ final ImagePaver paver = ImagePaver(
+ tar: tar,
+ sshKeyManagerProvider: sshKeyManagerProvider,
+ fs: fs,
+ processManager: processManager,
+ );
+
+ final OperationResult result = await paver.pave(
+ 'generic-x64.tgz',
+ deviceName,
+ verbose: false,
+ );
+
+ final List<String> capturedStartArgs = verify(
+ processManager.start(captureAny),
+ ).captured.cast<List<String>>().single;
+ expect(capturedStartArgs.first, endsWith('/pave.sh'));
+ expect(capturedStartArgs.skip(1).toList(), <String>[
+ '--fail-fast',
+ '-1',
+ '--allow-zedboot-version-mismatch',
+ '-n', deviceName, //
+ '--authorized-keys', '.ssh/authorized_keys',
+ ]);
+ expect(result.success, true);
+ });
+}
+
+class FakeTar implements Tar {
+ const FakeTar(this.passes, this.fs)
+ : assert(passes != null),
+ assert(fs != null);
+
+ final bool passes;
+ final MemoryFileSystem fs;
+
+ @override
+ Future<OperationResult> untar(String src, String destination,
+ {Duration timeoutMs}) async {
+ if (passes) {
+ final Directory dir = fs.directory(destination)
+ ..createSync(recursive: true);
+ dir.childFile('pave.sh').createSync();
+ return OperationResult.success();
+ }
+ return OperationResult.error('tar failed');
+ }
+}
+
+class FakeSshKeyManager implements SshKeyManager {
+ const FakeSshKeyManager(this.passes);
+
+ final bool passes;
+
+ @override
+ Future<OperationResult> createKeys({
+ String destinationPath = '.ssh',
+ bool force = false,
+ }) async {
+ if (passes) {
+ return OperationResult.success();
+ }
+ return OperationResult.error('ssh failed');
+ }
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/fuchsia_ctl/test/logger_test.dart b/packages/fuchsia_ctl/test/logger_test.dart
new file mode 100644
index 0000000..4e22f98
--- /dev/null
+++ b/packages/fuchsia_ctl/test/logger_test.dart
@@ -0,0 +1,58 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// @dart = 2.4
+import 'dart:io';
+
+import 'package:file/memory.dart';
+import 'package:fuchsia_ctl/src/logger.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('PrintLogger with file logs data correctly', () async {
+ final MemoryFileSystem fs = MemoryFileSystem();
+ fs.file('log.txt').createSync();
+ final IOSink data = fs.file('log.txt').openWrite();
+ final PrintLogger logger = PrintLogger(out: data, level: LogLevel.debug);
+ logger.debug('abc');
+ logger.info('cdf');
+ logger.warning('gh');
+ logger.error('jk');
+ await logger.flush();
+ final String content = fs.file('log.txt').readAsStringSync();
+ expect(content, contains('ERROR jk'));
+ expect(content, contains('INFO cdf'));
+ expect(content, contains('WARN gh'));
+ expect(content, contains('DEBUG abc'));
+ });
+ test('PrintLogger with no file logs data correctly', () async {
+ final PrintLogger logger = PrintLogger();
+ logger.debug('abc');
+ logger.info('cdf');
+ logger.warning('gh');
+ logger.error('jk');
+ final String outContent = logger.outputLog();
+ final String errContent = logger.errorLog();
+ expect(errContent, contains('jk\n'));
+ expect(outContent, contains('cdf\n'));
+ expect(outContent, contains('gh\n'));
+ expect(outContent, contains('abc\n'));
+ });
+
+ test('PrintLogger with file logs logs only data above level', () async {
+ final MemoryFileSystem fs = MemoryFileSystem();
+ fs.file('log.txt').createSync();
+ final IOSink data = fs.file('log.txt').openWrite();
+ final PrintLogger logger = PrintLogger(out: data, level: LogLevel.info);
+ logger.debug('abc');
+ logger.info('cdf');
+ logger.warning('gh');
+ logger.error('jk');
+ await logger.flush();
+ final String content = fs.file('log.txt').readAsStringSync();
+ expect(content, contains('ERROR jk'));
+ expect(content, contains('INFO cdf'));
+ expect(content, contains('WARN gh'));
+ expect(content, isNot(contains('DEBUG abc')));
+ });
+}
diff --git a/packages/fuchsia_ctl/test/package_server_test.dart b/packages/fuchsia_ctl/test/package_server_test.dart
new file mode 100644
index 0000000..7a49d23
--- /dev/null
+++ b/packages/fuchsia_ctl/test/package_server_test.dart
@@ -0,0 +1,146 @@
+// 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.
+// @dart = 2.4
+import 'dart:io' show ProcessResult;
+import 'dart:math' show Random;
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+import 'package:fuchsia_ctl/src/package_server.dart';
+import 'package:fuchsia_ctl/src/operation_result.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:test/test.dart';
+
+import 'fakes.dart';
+
+void main() {
+ const String pmBin = 'pm';
+ const String repoPath = '/repo';
+
+ test('newRepo', () async {
+ final MockProcessManager processManager = MockProcessManager();
+
+ when(processManager.run(any)).thenAnswer((_) async {
+ return ProcessResult(0, 0, 'good job', '');
+ });
+
+ final PackageServer server = PackageServer(
+ pmBin,
+ processManager: processManager,
+ );
+
+ final OperationResult result = await server.newRepo(repoPath);
+
+ final List<String> capturedStartArgs =
+ verify(processManager.run(captureAny))
+ .captured
+ .cast<List<String>>()
+ .single;
+
+ expect(capturedStartArgs, <String>[pmBin, 'newrepo', '-repo', repoPath]);
+ expect(result.success, true);
+ });
+
+ test('publishRepo', () async {
+ const String farFile = 'flutter_runner-0.far';
+ final MockProcessManager processManager = MockProcessManager();
+
+ when(processManager.run(any)).thenAnswer((_) async {
+ return ProcessResult(0, 0, 'good job', '');
+ });
+
+ final PackageServer server = PackageServer(
+ pmBin,
+ processManager: processManager,
+ );
+
+ final OperationResult result = await server.publishRepo(repoPath, farFile);
+
+ final List<String> capturedStartArgs =
+ verify(processManager.run(captureAny))
+ .captured
+ .cast<List<String>>()
+ .single;
+
+ expect(capturedStartArgs, <String>[
+ pmBin,
+ 'publish',
+ '-a',
+ '-repo',
+ repoPath,
+ '-f',
+ farFile,
+ ]);
+ expect(result.success, true);
+ });
+
+ test('serveRepo', () async {
+ final MockProcessManager processManager = MockProcessManager();
+ final int randomPort = Random().nextInt(60000);
+ final FakeProcess serverProcess = FakeProcess(
+ 0,
+ <String>[
+ '',
+ ],
+ <String>[''],
+ );
+
+ when(processManager.start(any)).thenAnswer((_) async {
+ return serverProcess;
+ });
+
+ final MemoryFileSystem fs = MemoryFileSystem();
+
+ final PackageServer server = PackageServer(
+ pmBin,
+ processManager: processManager,
+ fileSystem: fs,
+ );
+
+ expect(server.serving, false);
+ final File portFile = fs.file(
+ 'port.txt',
+ )
+ ..create()
+ ..writeAsString(
+ randomPort.toString(),
+ );
+
+ await server.serveRepo(
+ repoPath,
+ port: 0,
+ portFilePath: portFile.path,
+ );
+ expect(server.serving, true);
+
+ final List<String> capturedStartArgs =
+ verify(processManager.start(captureAny))
+ .captured
+ .cast<List<String>>()
+ .single;
+
+ expect(capturedStartArgs, <String>[
+ pmBin,
+ 'serve',
+ '-repo',
+ repoPath,
+ '-l',
+ ':0',
+ '-f',
+ 'port.txt',
+ ]);
+ expect(server.serverPort, randomPort);
+
+ final OperationResult result = await server.close();
+
+ expect(result.success, true);
+ expect(serverProcess.killed, true);
+
+ expect(server.serving, false);
+ });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/fuchsia_ctl/test/ssh_client_test.dart b/packages/fuchsia_ctl/test/ssh_client_test.dart
new file mode 100644
index 0000000..c5428c4
--- /dev/null
+++ b/packages/fuchsia_ctl/test/ssh_client_test.dart
@@ -0,0 +1,140 @@
+// 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.
+// @dart = 2.4
+import 'dart:async';
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+import 'package:fuchsia_ctl/src/ssh_client.dart';
+import 'package:fuchsia_ctl/src/operation_result.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:test/test.dart';
+
+import 'fakes.dart';
+
+void main() {
+ const String targetIp = '127.0.0.2';
+ const String identityFilePath = '.ssh/pkey';
+
+ test('interactive', () async {
+ final MockProcessManager processManager = MockProcessManager();
+
+ when(processManager.start(any)).thenAnswer((_) async {
+ return FakeProcess(0, <String>[''], <String>['']);
+ });
+
+ final SshClient ssh = SshClient(processManager: processManager);
+
+ final OperationResult result = await ssh.interactive(
+ targetIp,
+ identityFilePath: identityFilePath,
+ );
+
+ final List<String> capturedStartArgs =
+ verify(processManager.start(captureAny))
+ .captured
+ .cast<List<String>>()
+ .single;
+
+ expect(
+ capturedStartArgs,
+ ssh.getSshArguments(
+ identityFilePath: identityFilePath,
+ targetIp: targetIp,
+ ));
+ expect(result.success, true);
+ });
+
+ test('command', () async {
+ const List<String> command = <String>['ls', '-al'];
+ final MockProcessManager processManager = MockProcessManager();
+
+ when(processManager.start(any)).thenAnswer((_) async {
+ return FakeProcess(0, <String>['abc'], <String>['cdf']);
+ });
+
+ final SshClient ssh = SshClient(processManager: processManager);
+
+ final OperationResult result = await ssh.runCommand(
+ targetIp,
+ identityFilePath: identityFilePath,
+ command: command,
+ );
+
+ final List<String> capturedStartArgs =
+ verify(processManager.start(captureAny))
+ .captured
+ .cast<List<String>>()
+ .single;
+
+ expect(
+ capturedStartArgs,
+ ssh.getSshArguments(
+ identityFilePath: identityFilePath,
+ targetIp: targetIp,
+ command: command,
+ ));
+ expect(result.info, 'abc\n');
+ expect(result.success, true);
+ });
+
+ test('Command output is written to a log file', () async {
+ const List<String> command = <String>['ls', '-al'];
+ final MockProcessManager processManager = MockProcessManager();
+
+ when(processManager.start(any)).thenAnswer((_) async {
+ return FakeProcess(0, <String>['ef'], <String>['abc']);
+ });
+
+ final SshClient ssh = SshClient(processManager: processManager);
+ final FileSystem fs = MemoryFileSystem();
+ final OperationResult result = await ssh.runCommand(
+ targetIp,
+ identityFilePath: identityFilePath,
+ command: command,
+ fs: fs,
+ logFilePath: 'myfile.txt',
+ );
+
+ final String content = await fs.file('myfile.txt').readAsString();
+ expect(content, contains('ERROR abc'));
+ expect(content, contains('INFO ef'));
+ expect(result.success, true);
+ });
+
+ test('getSshArgs', () {
+ const SshClient ssh = SshClient();
+ final List<String> args = ssh.getSshArguments(
+ identityFilePath: identityFilePath,
+ targetIp: targetIp,
+ command: const <String>['ls', '-al'],
+ );
+ expect(args.last, 'ls -al');
+ });
+
+ test('sshCommand times out', () async {
+ final MockProcessManager processManager = MockProcessManager();
+
+ when(processManager.start(any)).thenAnswer((_) async {
+ await Future<void>.delayed(const Duration(milliseconds: 3));
+ return FakeProcess(0, <String>[''], <String>['']);
+ });
+
+ final SshClient ssh = SshClient(processManager: processManager);
+ try {
+ await ssh.runCommand(
+ targetIp,
+ identityFilePath: identityFilePath,
+ command: const <String>['ls', '-al'],
+ timeoutMs: const Duration(milliseconds: 1),
+ );
+ } catch (e) {
+ expect(e, isA<TimeoutException>());
+ }
+ });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/fuchsia_ctl/test/ssh_key_manager_test.dart b/packages/fuchsia_ctl/test/ssh_key_manager_test.dart
new file mode 100644
index 0000000..37c773c
--- /dev/null
+++ b/packages/fuchsia_ctl/test/ssh_key_manager_test.dart
@@ -0,0 +1,63 @@
+// @dart = 2.4
+import 'dart:io';
+
+import 'package:file/file.dart';
+import 'package:path/path.dart' as path;
+import 'package:file/memory.dart';
+import 'package:fuchsia_ctl/src/ssh_key_manager.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('SystemSshKeyManager', () {
+ MemoryFileSystem fs;
+ final MockProcessManager processManager = MockProcessManager();
+
+ setUp(() async {
+ fs = MemoryFileSystem();
+ });
+ test('CreateAuthorizedKeys', () async {
+ const SystemSshKeyManager systemSshKeyManager = SystemSshKeyManager();
+ final File authorizedKeys = fs.file('authorized_keys');
+ final File pkeyPub = fs.file('key.pub');
+ pkeyPub.writeAsString('ssh-rsa AAAA== abc@cde.com');
+ systemSshKeyManager.createAuthorizedKeys(authorizedKeys, pkeyPub);
+ final String result = await authorizedKeys.readAsString();
+ expect(result, equals('ssh-rsa AAAA==\n'));
+ });
+
+ test('KeysNotGenerated_PubKeyPassed', () async {
+ final File pkeyPub = fs.file('key.pub');
+ pkeyPub.writeAsString('ssh-rsa AAAA== abc@cde.com');
+ final SystemSshKeyManager systemSshKeyManager = SystemSshKeyManager(
+ processManager: processManager, fs: fs, pkeyPubPath: pkeyPub.path);
+ await systemSshKeyManager.createKeys();
+ final File authorizedKeys = fs.file(path.join('.ssh', 'authorized_keys'));
+ final String result = await authorizedKeys.readAsString();
+ expect(result, equals('ssh-rsa AAAA==\n'));
+ verifyNever(processManager.run(any));
+ });
+
+ test('KeysGenerated', () async {
+ when(processManager.run(any)).thenAnswer((_) async {
+ final File pkeyPub = fs.file(path.join('.ssh', 'pkey.pub'));
+ pkeyPub.writeAsString('ssh-rsa AAAA== abc@cde.com');
+ final File pkey = fs.file(path.join('.ssh', 'pkey'));
+ pkey.create();
+ return ProcessResult(0, 0, 'Good job', '');
+ });
+ final SystemSshKeyManager systemSshKeyManager =
+ SystemSshKeyManager(processManager: processManager, fs: fs);
+ await systemSshKeyManager.createKeys();
+ final File authorizedKeys = fs.file(path.join('.ssh', 'authorized_keys'));
+ expect(await authorizedKeys.exists(), isTrue);
+ final File pkey = fs.file(path.join('.ssh', 'pkey'));
+ expect(await pkey.exists(), isTrue);
+ final File pkeyPub = fs.file(path.join('.ssh', 'pkey.pub'));
+ expect(await pkeyPub.exists(), isTrue);
+ });
+ });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/fuchsia_ctl/test/tar_test.dart b/packages/fuchsia_ctl/test/tar_test.dart
new file mode 100644
index 0000000..ee08006
--- /dev/null
+++ b/packages/fuchsia_ctl/test/tar_test.dart
@@ -0,0 +1,37 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// @dart = 2.4
+import 'dart:async';
+import 'dart:io';
+
+import 'package:process/process.dart';
+import 'package:test/test.dart';
+import 'package:fuchsia_ctl/src/tar.dart';
+import 'package:mockito/mockito.dart';
+
+void main() {
+ group('Tar', () {
+ test('Untar file times out', () async {
+ final MockProcessManager processManager = MockProcessManager();
+
+ when(processManager.run(any)).thenAnswer((_) async {
+ await Future<void>.delayed(const Duration(milliseconds: 3));
+ return ProcessResult(0, 0, 'Good job', '');
+ });
+
+ final Tar tar = SystemTar(processManager: processManager);
+ try {
+ await tar.untar(
+ 'source.tar',
+ '/destination',
+ timeoutMs: const Duration(milliseconds: 1),
+ );
+ } catch (e) {
+ expect(e, isA<TimeoutException>());
+ }
+ });
+ });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/fuchsia_ctl/tool/build.sh b/packages/fuchsia_ctl/tool/build.sh
new file mode 100755
index 0000000..8b0d0ad
--- /dev/null
+++ b/packages/fuchsia_ctl/tool/build.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+# 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.
+
+set -e
+
+command -v cipd > /dev/null || {
+ echo "Please install CIPD (available from depot_tools) and add to path first.";
+ exit -1;
+}
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
+
+cipd ensure -ensure-file $DIR/ensure_file -root $DIR
+
+pushd $DIR/..
+
+if [[ -d "build" ]]; then
+ echo "Please remove the build directory before proceeding"
+ exit -1
+fi
+
+mkdir -p build
+
+tool/dart-sdk/bin/pub get
+
+tool/dart-sdk/bin/dart2native bin/main.dart -o build/fuchsia_ctl
+cp -f LICENSE build/
+
+popd
diff --git a/packages/fuchsia_ctl/tool/ensure_file b/packages/fuchsia_ctl/tool/ensure_file
new file mode 100644
index 0000000..5d31ed5
--- /dev/null
+++ b/packages/fuchsia_ctl/tool/ensure_file
@@ -0,0 +1,3 @@
+$ServiceURL https://chrome-infra-packages.appspot.com/
+
+dart/dart-sdk/${os}-${arch} stable
\ No newline at end of file
diff --git a/packages/imitation_game/CHANGELOG.md b/packages/imitation_game/CHANGELOG.md
new file mode 100644
index 0000000..d775621
--- /dev/null
+++ b/packages/imitation_game/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.0.1
+
+* Initial version (TBD).
diff --git a/packages/imitation_game/LICENSE b/packages/imitation_game/LICENSE
new file mode 100644
index 0000000..bc67b8f
--- /dev/null
+++ b/packages/imitation_game/LICENSE
@@ -0,0 +1,27 @@
+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
diff --git a/packages/imitation_game/README.md b/packages/imitation_game/README.md
new file mode 100644
index 0000000..c416f83
--- /dev/null
+++ b/packages/imitation_game/README.md
@@ -0,0 +1,110 @@
+# Imitation Game
+
+## Description
+
+`imitation_game` is a platform for performing automated tests to compare the
+performance of different UI frameworks. For example, how much memory does the
+same app written in Flutter and UIKit take?
+
+## Running all the tests
+
+You need a mobile device plugged into your computer and setup for development.
+The mobile device and the computer need to be on the same network, one that
+allows communication between computers since that's how the mobile phone will
+report its results to the computer.
+
+```sh
+dart imitation_game.dart
+```
+
+## Dependencies
+
+In order to run the tests you will need the union of all the platforms being
+tested. As new tests are added please add to this list:
+
+### iOS
+
+- Flutter
+- Xcode
+- [ios_deploy](https://github.com/ios-control/ios-deploy) - used to launch apps
+ on the attached iOS device.
+
+## Example File Layout
+
+```text
+./
+├─ imitation_game.dart
+└─ imitation_tests/
+ ├─ smiley/
+ │ ├─ README.md
+ │ ├─ flutter/
+ │ │ ├─ run_ios.sh
+ │ │ └─ <flutter project files>
+ │ └─ uikit/
+ │ ├─ run_ios.sh
+ │ └─ <uikit project files>
+ └─ memory/
+ ├─ README.md
+ ├─ flutter/
+ │ ├─ run_ios.sh
+ │ └─ <flutter project files>
+ └─ uikit/
+ ├─ run_ios.sh
+ └─ <uikit project files>
+```
+
+Here there are 2 different tests with 2 different platform implementations. The
+tests are named `smiley` and `memory`, they are both implemented on the
+platforms `flutter` and `uikit`.
+
+### Adding a test
+
+Tests should comprise of implementations on one or more platform. The directory
+for the test should be added to `./tests`. Inside that directory there should
+be a directory of implementations and a `README.md` file that explains the test.
+
+### Adding an implementation to a test
+
+An implementation has to follow these rules:
+
+- It needs to perform the same operations as the other implementations and
+ follow the description in the test's `README.md`.
+- It needs to contain a `run_ios.sh` script that will build and launch the test
+ on the connected device.
+- It should contain a file named `ip.txt` which will be overwritten by
+ `imitation_game.dart` with the ip address and port that should be used to
+ report results to.
+- It needs to report its results to the ip and port in the `ip.txt` via an HTTP
+ POST of JSON data.
+
+## Data format for results
+
+```json
+{
+ "test": "name_of_test",
+ "platform": "name_of_platform",
+ "results": {
+ "some_result_name": 1.23,
+ "some_result_name2": 4.56,
+ }
+}
+```
+
+A single test run can report multiple numbers.
+
+## Results
+Date created: 2020-09-09 17:18:15.594586Z
+Flutter 1.22.0-10.0.pre.95 • channel master • https://github.com/flutter/flutter.git
+Framework • revision d2fa384c31 (21 hours ago) • 2020-09-08 13:15:06 -0700
+Engine • revision d1d848e842
+Tools • Dart 2.10.0 (build 2.10.0-98.0.dev)
+
+- smiley
+ - flutter
+ - ios_startup_time: 0.691717
+ - adb_memory_total: 43179.0
+ - uikit
+ - ios_startup_time: 0.2632870674133301
+ - android
+ - adb_memory_total: 97941.0
+
diff --git a/packages/imitation_game/bin/imitation_game.dart b/packages/imitation_game/bin/imitation_game.dart
new file mode 100644
index 0000000..4bfa7e7
--- /dev/null
+++ b/packages/imitation_game/bin/imitation_game.dart
@@ -0,0 +1,278 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'package:mustache/mustache.dart';
+import 'package:imitation_game/readme_template.dart';
+import 'package:args/args.dart';
+
+// ignore_for_file: avoid_as
+
+const int _port = 4040;
+
+Future<String> _findIpAddress() async {
+ String result;
+ final List<NetworkInterface> interfaces = await NetworkInterface.list();
+ for (final NetworkInterface interface in interfaces) {
+ for (final InternetAddress address in interface.addresses) {
+ if (address.type == InternetAddressType.IPv4) {
+ // TODO(gaaclarke): Implment having multiple addresses.
+ assert(result == null);
+ result = address.address;
+ }
+ }
+ }
+ return result;
+}
+
+Future<String> _getFlutterVersion() async {
+ String result = '';
+ final Process flutterVersion =
+ await Process.start('flutter', <String>['--version']);
+ flutterVersion.stdout.transform(utf8.decoder).listen((String event) {
+ result += event;
+ });
+ await flutterVersion.exitCode;
+ return result.trim();
+}
+
+typedef FileFilter = bool Function(FileSystemEntity);
+Future<List<FileSystemEntity>> findFiles(Directory dir, {FileFilter where}) {
+ final List<FileSystemEntity> files = <FileSystemEntity>[];
+ final Completer<List<FileSystemEntity>> completer =
+ Completer<List<FileSystemEntity>>();
+ final Stream<FileSystemEntity> lister = dir.list(recursive: true);
+ lister.listen((FileSystemEntity file) {
+ if (where == null || where(file)) {
+ files.add(file);
+ }
+ }, onDone: () => completer.complete(files));
+ return completer.future;
+}
+
+Future<String> _makeMarkdownOutput(Map<String, dynamic> results) async {
+ // TODO(gaaclarke): Add the Flutter version.
+ final Template template = Template(readmeTemplate, name: 'README.md');
+ final Map<String, dynamic> values = Map<String, dynamic>.from(results);
+ values['date'] = DateTime.now().toUtc();
+ values['flutterVersion'] = await _getFlutterVersion();
+ final String output = template.renderString(values);
+ return output;
+}
+
+/// This merges [newResults] into [oldResults] such the union of the keys will
+/// have their values from [newResults] and the symmetric difference will have the
+/// value from their respective sets.
+Map<String, dynamic> _integrate(
+ {Map<String, dynamic> oldResults, Map<String, dynamic> newResults}) {
+ final Map<String, dynamic> result = Map<String, dynamic>.from(oldResults);
+ newResults.forEach((String test, dynamic testValue) {
+ final Map<String, dynamic> testMap = testValue as Map<String, dynamic>;
+ testMap.forEach((String platform, dynamic platformValue) {
+ final Map<String, dynamic> platformMap =
+ platformValue as Map<String, dynamic>;
+ platformMap.forEach((String measurement, dynamic measurementValue) {
+ if (!result.containsKey(test)) {
+ result[test] = <String, dynamic>{};
+ }
+ if (!result[test].containsKey(platform)) {
+ result[test][platform] = <String, dynamic>{};
+ }
+ result[test][platform][measurement] = measurementValue;
+ });
+ });
+ });
+ return result;
+}
+
+class _Script {
+ _Script({this.path});
+ String path;
+}
+
+class _ScriptRunner {
+ _ScriptRunner(this._scriptPaths);
+
+ final List<String> _scriptPaths;
+ Process _currentProcess;
+ StreamSubscription<String> _stdoutSubscription;
+ StreamSubscription<String> _stderrSubscription;
+
+ Future<_Script> runNext() async {
+ if (_currentProcess != null) {
+ _stdoutSubscription.cancel();
+ _stderrSubscription.cancel();
+ _currentProcess.kill();
+ _currentProcess = null;
+ }
+
+ if (_scriptPaths.isEmpty) {
+ return null;
+ } else {
+ final String path = _scriptPaths.last;
+ print('running: $path');
+ _scriptPaths.removeLast();
+ _currentProcess = await Process.start('sh', <String>[path]);
+ // TODO(gaaclarke): Implement a timeout.
+ _stdoutSubscription =
+ _currentProcess.stdout.transform(utf8.decoder).listen((String data) {
+ print(data);
+ });
+ _stderrSubscription =
+ _currentProcess.stderr.transform(utf8.decoder).listen((String data) {
+ print(data);
+ });
+ return _Script(path: path);
+ }
+ }
+}
+
+/// Recursively converts a map of maps to a map of lists of maps.
+///
+/// For example:
+/// _map2List({'a': {'b': 123}}, ['foo', 'bar']) ->
+/// {
+/// 'foo':[
+/// {
+/// 'name': 'a',
+/// 'bar': [{'name': 'b', 'value': 123}]
+/// }
+/// ]
+/// }
+Map<String, dynamic> _map2List(Map<String, dynamic> map, List<String> names) {
+ final List<Map<String, dynamic>> returnList = <Map<String, dynamic>>[];
+ final List<String> tail = names.sublist(1);
+ map.forEach((String key, dynamic value) {
+ final Map<String, dynamic> testResult = <String, dynamic>{'name': key};
+ if (tail.isEmpty) {
+ testResult['value'] = value;
+ } else {
+ testResult[tail.first] = _map2List(value, tail)[tail.first];
+ }
+ returnList.add(testResult);
+ });
+ return <String, dynamic>{names.first: returnList};
+}
+
+class _ImitationGame {
+ final Map<String, dynamic> results = <String, dynamic>{};
+ _ScriptRunner _scriptRunner;
+ _Script _currentScript;
+
+ Future<bool> start(List<String> iosScripts) {
+ _scriptRunner = _ScriptRunner(iosScripts);
+ return _runNext();
+ }
+
+ Future<bool> handleResult(Map<String, dynamic> data) {
+ final String test = data['test'];
+ final String platform = data['platform'];
+ results.putIfAbsent(test, () => <String, dynamic>{});
+ results[test].putIfAbsent(platform, () => <String, dynamic>{});
+ data['results'].forEach((String k, dynamic v) {
+ results[test][platform][k] = v as double;
+ });
+ return _runNext();
+ }
+
+ Future<bool> handleTimeout() {
+ return _runNext();
+ }
+
+ Future<bool> _runNext() async {
+ _currentScript = await _scriptRunner.runNext();
+ return _currentScript != null;
+ }
+}
+
+enum _TargetPlatform { ANDROID, IOS }
+
+Future<void> main(List<String> args) async {
+ final ArgParser parser = ArgParser();
+ parser.addOption('platform',
+ allowed: <String>['android', 'ios'], defaultsTo: 'android');
+ final ArgResults parserResults = parser.parse(args);
+ final _TargetPlatform targetPlatform = parserResults['platform'] == 'android'
+ ? _TargetPlatform.ANDROID
+ : _TargetPlatform.IOS;
+
+ final HttpServer server = await HttpServer.bind(
+ InternetAddress.anyIPv4,
+ _port,
+ );
+ final String ipaddress = await _findIpAddress();
+ print('Listening on $ipaddress:${server.port}');
+
+ for (final FileSystemEntity entity in await findFiles(Directory.current,
+ where: (FileSystemEntity f) => f.path.endsWith('ip.txt'))) {
+ final File file = File(entity.path);
+ file.writeAsStringSync('$ipaddress:${server.port}');
+ }
+
+ final String scriptName = (targetPlatform == _TargetPlatform.ANDROID)
+ ? 'run_android.sh'
+ : () {
+ assert(targetPlatform == _TargetPlatform.IOS);
+ return 'run_ios.sh';
+ }();
+ final List<String> scripts = (await findFiles(Directory.current,
+ where: (FileSystemEntity f) => f.path.endsWith(scriptName)))
+ .map((FileSystemEntity e) => e.path)
+ .toList();
+
+ if (scripts.isEmpty) {
+ return;
+ }
+
+ final _ImitationGame game = _ImitationGame();
+ bool keepRunning = await game.start(scripts);
+
+ while (keepRunning) {
+ try {
+ final Stream<HttpRequest> timeoutServer = server.timeout(
+ const Duration(minutes: 5), onTimeout: (EventSink<HttpRequest> sink) {
+ print('TIMEOUT!');
+ throw TimeoutException('timeout');
+ });
+ await for (final HttpRequest request in timeoutServer) {
+ print('got request: ${request.method}');
+ if (request.method == 'POST') {
+ final String content = await utf8.decoder.bind(request).join();
+ final Map<String, dynamic> data =
+ jsonDecode(content) as Map<String, dynamic>;
+ print('$data');
+ keepRunning = await game.handleResult(data);
+ if (!keepRunning) {
+ break;
+ }
+ } else {
+ request.response.write('use post');
+ }
+ await request.response.close();
+ }
+ } on TimeoutException catch (_) {
+ keepRunning = await game.handleTimeout();
+ }
+ }
+
+ // TODO(gaaclarke): Add a log of what Flutter version generated the numbers.
+ const String lastResultsFilename = 'last_results.json';
+ const JsonDecoder decoder = JsonDecoder();
+ final Map<String, dynamic> lastResults =
+ decoder.convert(File(lastResultsFilename).readAsStringSync())
+ as Map<String, dynamic>;
+
+ // TODO(aaclarke): Calculate the generation time for each measurement since we
+ // can't generate everything in one pass (because you are running iOS or Android).
+ final Map<String, dynamic> totalResults =
+ _integrate(newResults: game.results, oldResults: lastResults);
+
+ const JsonEncoder encoder = JsonEncoder.withIndent(' ');
+ final String jsonResults = encoder.convert(totalResults);
+ File(lastResultsFilename).writeAsStringSync(jsonResults);
+
+ final Map<String, dynamic> markdownValues =
+ _map2List(totalResults, <String>['tests', 'platforms', 'measurements']);
+ File('README.md')
+ .writeAsStringSync(await _makeMarkdownOutput(markdownValues));
+ await server.close(force: true);
+}
diff --git a/packages/imitation_game/helper/run_android_helper.sh b/packages/imitation_game/helper/run_android_helper.sh
new file mode 100644
index 0000000..1137d54
--- /dev/null
+++ b/packages/imitation_game/helper/run_android_helper.sh
@@ -0,0 +1,25 @@
+report_results()
+{
+ local TEST=$1
+ local PLATFORM=$2
+ local MEASUREMENTS=$3
+ curl --header "Content-Type: application/json" \
+ --request POST \
+ --data "{\"test\":\"$TEST\",\"platform\":\"$PLATFORM\",\"results\":$MEASUREMENTS}" \
+ http://localhost:4040
+}
+
+launch_apk()
+{
+ local APK_PATH=$1
+ local BUNDLE_NAME=$2
+ local ACTIVITY_NAME=$3
+ adb install -r $APK_PATH
+ adb shell am start -n $BUNDLE_NAME/$BUNDLE_NAME.$ACTIVITY_NAME
+}
+
+read_app_total_memory()
+{
+ local BUNDLE_NAME=$1
+ adb shell dumpsys meminfo $BUNDLE_NAME | sed -n 's/.*TOTAL:[ ]*\([0-9]*\).*/\1/p'
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/README.md b/packages/imitation_game/imitation_tests/smiley/README.md
new file mode 100644
index 0000000..220c40d
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/README.md
@@ -0,0 +1,11 @@
+# Smiley
+
+This test is an app that just draws one large image to the screen `smiley.png`,
+stretched to fit inside the screen.
+
+The following things are measured:
+
+- 'ios_startup_time' - The time between the start of the process and the
+ rendering of the image to the screen.
+- 'adb_total_memory' - The amount of system memory the app is using after having
+ rendered the image to the screen.
diff --git a/packages/imitation_game/imitation_tests/smiley/android/.gitignore b/packages/imitation_game/imitation_tests/smiley/android/.gitignore
new file mode 100644
index 0000000..603b140
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/.gitignore
@@ -0,0 +1,14 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/.gitignore b/packages/imitation_game/imitation_tests/smiley/android/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/build.gradle b/packages/imitation_game/imitation_tests/smiley/android/app/build.gradle
new file mode 100644
index 0000000..ca31675
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/build.gradle
@@ -0,0 +1,38 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.2"
+
+ defaultConfig {
+ applicationId "dev.flutter.imitation_game.smiley"
+ minSdkVersion 22
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ signingConfig signingConfigs.debug
+ }
+ }
+
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.0.2'
+ implementation 'androidx.core:core-ktx:1.0.2'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/proguard-rules.pro b/packages/imitation_game/imitation_tests/smiley/android/app/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/androidTest/java/dev/flutter/imitation_game/smiley/ExampleInstrumentedTest.kt b/packages/imitation_game/imitation_tests/smiley/android/app/src/androidTest/java/dev/flutter/imitation_game/smiley/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..f276ddb
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/androidTest/java/dev/flutter/imitation_game/smiley/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package dev.flutter.imitation_game.smiley
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("dev.flutter.imitation_game.smiley", appContext.packageName)
+ }
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/AndroidManifest.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0a91cd9
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="dev.flutter.imitation_game.smiley">
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme">
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
\ No newline at end of file
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/java/dev/flutter/imitation_game/smiley/MainActivity.kt b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/java/dev/flutter/imitation_game/smiley/MainActivity.kt
new file mode 100644
index 0000000..7d4ef8a
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/java/dev/flutter/imitation_game/smiley/MainActivity.kt
@@ -0,0 +1,15 @@
+package dev.flutter.imitation_game.smiley
+
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.view.View
+
+class MainActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
+ actionBar?.hide()
+ setContentView(R.layout.activity_main)
+ }
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="85.84757"
+ android:endY="92.4963"
+ android:startX="42.9492"
+ android:startY="49.59793"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0" />
+ <item
+ android:color="#00000000"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000" />
+</vector>
\ No newline at end of file
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable/ic_launcher_background.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M9,0L9,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,0L19,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,0L29,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,0L39,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,0L49,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,0L59,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,0L69,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,0L79,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M89,0L89,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M99,0L99,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,9L108,9"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,19L108,19"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,29L108,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,39L108,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,49L108,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,59L108,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,69L108,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,79L108,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,89L108,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,99L108,99"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,29L89,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,39L89,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,49L89,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,59L89,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,69L89,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,79L89,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,19L29,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,19L39,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,19L49,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,19L59,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,19L69,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,19L79,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+</vector>
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable/smiley.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable/smiley.png
new file mode 100644
index 0000000..441b66a
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable/smiley.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/layout/activity_main.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..4820781
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".MainActivity">
+
+ <ImageView
+ android:id="@+id/imageView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="fitCenter"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/smiley" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a571e60
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..61da551
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c41dd28
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..db5080a
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..6dba46d
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..da31a87
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..15ac681
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b216f2d
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..f25a419
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..e96783c
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/colors.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..030098f
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#6200EE</color>
+ <color name="colorPrimaryDark">#3700B3</color>
+ <color name="colorAccent">#03DAC5</color>
+</resources>
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/strings.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..1c84e3b
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+ <string name="app_name">Smiley</string>
+</resources>
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/styles.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..7af3eba
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,13 @@
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="@style/Theme.AppCompat.Light.NoActionBar">
+ <!-- Customize your theme here. -->
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</item>
+ <item name="windowNoTitle">true</item>
+ <item name="android:windowNoTitle">true</item>
+ </style>
+
+</resources>
diff --git a/packages/imitation_game/imitation_tests/smiley/android/build.gradle b/packages/imitation_game/imitation_tests/smiley/android/build.gradle
new file mode 100644
index 0000000..067a362
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/build.gradle
@@ -0,0 +1,29 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ ext.kotlin_version = '1.3.61'
+ repositories {
+ google()
+ jcenter()
+
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.6.2'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/android/gradle.properties b/packages/imitation_game/imitation_tests/smiley/android/gradle.properties
new file mode 100644
index 0000000..23339e0
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
diff --git a/packages/imitation_game/imitation_tests/smiley/android/gradle/wrapper/gradle-wrapper.jar b/packages/imitation_game/imitation_tests/smiley/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/gradle/wrapper/gradle-wrapper.properties b/packages/imitation_game/imitation_tests/smiley/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..67a69c7
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Aug 18 13:30:11 PDT 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
diff --git a/packages/imitation_game/imitation_tests/smiley/android/gradlew b/packages/imitation_game/imitation_tests/smiley/android/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/packages/imitation_game/imitation_tests/smiley/android/gradlew.bat b/packages/imitation_game/imitation_tests/smiley/android/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/packages/imitation_game/imitation_tests/smiley/android/run_android.sh b/packages/imitation_game/imitation_tests/smiley/android/run_android.sh
new file mode 100755
index 0000000..de66f41
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/run_android.sh
@@ -0,0 +1,12 @@
+set -e
+cd $( dirname "${BASH_SOURCE[0]}" )
+source ../../../helper/run_android_helper.sh
+
+readonly BUNDLE="dev.flutter.imitation_game.smiley"
+readonly TEST_NAME="smiley"
+
+./gradlew assembleRelease
+launch_apk ./app/build/outputs/apk/release/app-release.apk $BUNDLE "MainActivity"
+sleep 10
+MEMORY_TOTAL=`read_app_total_memory $BUNDLE`
+report_results $TEST_NAME "android" "{\"adb_memory_total\":$MEMORY_TOTAL.0}"
diff --git a/packages/imitation_game/imitation_tests/smiley/android/settings.gradle b/packages/imitation_game/imitation_tests/smiley/android/settings.gradle
new file mode 100644
index 0000000..21c96b7
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name='Smiley'
+include ':app'
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/run_android.sh b/packages/imitation_game/imitation_tests/smiley/flutter/run_android.sh
new file mode 100755
index 0000000..cd22623
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/run_android.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+set -e
+cd $( dirname "${BASH_SOURCE[0]}" )
+source ../../../helper/run_android_helper.sh
+
+readonly BUNDLE="com.example.smiley"
+readonly TEST_NAME="smiley"
+
+cd $( dirname "${BASH_SOURCE[0]}" )
+cd smiley
+flutter build apk
+launch_apk ./build/app/outputs/flutter-apk/app-release.apk $BUNDLE "MainActivity"
+sleep 10
+MEMORY_TOTAL=`read_app_total_memory $BUNDLE`
+report_results $TEST_NAME "flutter" "{\"adb_memory_total\":$MEMORY_TOTAL.0}"
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/run_ios.sh b/packages/imitation_game/imitation_tests/smiley/flutter/run_ios.sh
new file mode 100755
index 0000000..decb485
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/run_ios.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+cd $( dirname "${BASH_SOURCE[0]}" )
+cd smiley
+flutter run --release
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/.gitignore b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/.gitignore
new file mode 100644
index 0000000..f3c2053
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/.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/imitation_game/imitation_tests/smiley/flutter/smiley/.metadata b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/.metadata
new file mode 100644
index 0000000..3f3cbf6
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/.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: 227990ab308574184e06944710bae000330412d0
+ channel: unknown
+
+project_type: app
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/README.md b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/README.md
new file mode 100644
index 0000000..683f68d
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/README.md
@@ -0,0 +1,16 @@
+# smiley
+
+A new Flutter project.
+
+## 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/imitation_game/imitation_tests/smiley/flutter/smiley/android/.gitignore b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/.gitignore
new file mode 100644
index 0000000..0a741cb
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/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/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/build.gradle b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/build.gradle
new file mode 100644
index 0000000..01823ad
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/build.gradle
@@ -0,0 +1,63 @@
+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.smiley"
+ minSdkVersion 16
+ targetSdkVersion 28
+ 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 '../..'
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/debug/AndroidManifest.xml b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..8177932
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.smiley">
+ <!-- Flutter needs it to communicate with the running application
+ to allow setting breakpoints, to provide hot reload, etc.
+ -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/AndroidManifest.xml b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d377ac5
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,47 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.smiley">
+ <!-- io.flutter.app.FlutterApplication is an android.app.Application that
+ calls FlutterMain.startInitialization(this); in its onCreate method.
+ In most cases you can leave this as-is, but you if you want to provide
+ additional functionality it is fine to subclass or reimplement
+ FlutterApplication and put your custom class here. -->
+ <application
+ android:name="io.flutter.app.FlutterApplication"
+ android:label="smiley"
+ android:icon="@mipmap/ic_launcher">
+ <activity
+ android:name=".MainActivity"
+ android:launchMode="singleTop"
+ android:theme="@style/LaunchTheme"
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+ android:hardwareAccelerated="true"
+ android:windowSoftInputMode="adjustResize">
+ <!-- Specifies an Android theme to apply to this Activity as soon as
+ the Android process has started. This theme is visible to the user
+ while the Flutter UI initializes. After that, this theme continues
+ to determine the Window background behind the Flutter UI. -->
+ <meta-data
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme"
+ />
+ <!-- Displays an Android View that continues showing the launch screen
+ Drawable until Flutter paints its first frame, then this splash
+ screen fades out. A splash screen is useful to avoid any visual
+ gap between the end of Android's launch screen and the painting of
+ Flutter's first frame. -->
+ <meta-data
+ android:name="io.flutter.embedding.android.SplashScreenDrawable"
+ android:resource="@drawable/launch_background"
+ />
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <!-- Don't delete the meta-data below.
+ This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+ <meta-data
+ android:name="flutterEmbedding"
+ android:value="2" />
+ </application>
+</manifest>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/kotlin/com/example/smiley/MainActivity.kt b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/kotlin/com/example/smiley/MainActivity.kt
new file mode 100644
index 0000000..ccd14ef
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/kotlin/com/example/smiley/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.example.smiley
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/drawable/launch_background.xml b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@android:color/white" />
+
+ <!-- You can insert your own image assets here -->
+ <!-- <item>
+ <bitmap
+ android:gravity="center"
+ android:src="@mipmap/launch_image" />
+ </item> -->
+</layer-list>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/values/styles.xml b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..1f83a33
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Theme applied to the Android Window while the process is starting -->
+ <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+ <!-- Show a splash screen on the activity. Automatically removed when
+ Flutter draws its first frame -->
+ <item name="android:windowBackground">@drawable/launch_background</item>
+ </style>
+ <!-- Theme applied to the Android Window as soon as the process has started.
+ This theme determines the color of the Android Window while your
+ Flutter UI initializes, as well as behind your Flutter UI while its
+ running.
+
+ This Theme is only used starting with V2 of Flutter's Android embedding. -->
+ <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+ <item name="android:windowBackground">@android:color/white</item>
+ </style>
+</resources>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/profile/AndroidManifest.xml b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..8177932
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.smiley">
+ <!-- Flutter needs it to communicate with the running application
+ to allow setting breakpoints, to provide hot reload, etc.
+ -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/build.gradle b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/build.gradle
new file mode 100644
index 0000000..3100ad2
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/build.gradle
@@ -0,0 +1,31 @@
+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/imitation_game/imitation_tests/smiley/flutter/smiley/android/gradle.properties b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/gradle.properties
new file mode 100644
index 0000000..38c8d45
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/gradle/wrapper/gradle-wrapper.properties b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..296b146
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/settings.gradle b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/settings.gradle
new file mode 100644
index 0000000..44e62bc
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/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/imitation_game/imitation_tests/smiley/flutter/smiley/assets/ip.txt b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/assets/ip.txt
new file mode 100644
index 0000000..d331e21
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/assets/ip.txt
@@ -0,0 +1 @@
+192.168.0.18:4040
\ No newline at end of file
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/images/smiley.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/images/smiley.png
new file mode 100644
index 0000000..441b66a
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/images/smiley.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/.gitignore b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/.gitignore
new file mode 100644
index 0000000..e96ef60
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/.gitignore
@@ -0,0 +1,32 @@
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/AppFrameworkInfo.plist b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..6b4c0f7
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>App</string>
+ <key>CFBundleIdentifier</key>
+ <string>io.flutter.flutter.app</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>App</string>
+ <key>CFBundlePackageType</key>
+ <string>FMWK</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>MinimumOSVersion</key>
+ <string>8.0</string>
+</dict>
+</plist>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/Debug.xcconfig b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..e8efba1
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/Release.xcconfig b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..399e934
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Podfile b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Podfile
new file mode 100644
index 0000000..6697f0a
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Podfile
@@ -0,0 +1,87 @@
+# 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 parse_KV_file(file, separator='=')
+ file_abs_path = File.expand_path(file)
+ if !File.exists? file_abs_path
+ return [];
+ end
+ generated_key_values = {}
+ skip_line_start_symbols = ["#", "/"]
+ File.foreach(file_abs_path) do |line|
+ next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
+ plugin = line.split(pattern=separator)
+ if plugin.length == 2
+ podname = plugin[0].strip()
+ path = plugin[1].strip()
+ podpath = File.expand_path("#{path}", file_abs_path)
+ generated_key_values[podname] = podpath
+ else
+ puts "Invalid plugin specification: #{line}"
+ end
+ end
+ generated_key_values
+end
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ # Flutter Pod
+
+ copied_flutter_dir = File.join(__dir__, 'Flutter')
+ copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
+ copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
+ unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
+ # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
+ # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
+ # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
+
+ generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+ generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
+ cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];
+
+ unless File.exist?(copied_framework_path)
+ FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
+ end
+ unless File.exist?(copied_podspec_path)
+ FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
+ end
+ end
+
+ # Keep pod path relative so it can be checked into Podfile.lock.
+ pod 'Flutter', :path => 'Flutter'
+
+ # Plugin Pods
+
+ # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
+ # referring to absolute paths on developers' machines.
+ system('rm -rf .symlinks')
+ system('mkdir -p .symlinks/plugins')
+ plugin_pods = parse_KV_file('../.flutter-plugins')
+ plugin_pods.each do |name, path|
+ symlink = File.join('.symlinks', 'plugins', name)
+ File.symlink(path, symlink)
+ pod name, :path => File.join(symlink, 'ios')
+ end
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ target.build_configurations.each do |config|
+ config.build_settings['ENABLE_BITCODE'] = 'NO'
+ end
+ end
+end
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.pbxproj b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..24067aa
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,576 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 50;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+ E871A0843F25069A572B8976 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6ED58CA389D243E9D25A3BCF /* Pods_Runner.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 = "<group>"; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
+ 1653BF8CF7D70B08243DF6A2 /* 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 = "<group>"; };
+ 1A3DE4B0B684E0679C64523E /* 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 = "<group>"; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+ 6ED58CA389D243E9D25A3BCF /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
+ 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 = "<group>"; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ C4C62015C2EE2797ABF091BA /* 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 = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E871A0843F25069A572B8976 /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 2BE839568BFB70043910F5F6 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 6ED58CA389D243E9D25A3BCF /* Pods_Runner.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+ 8E5CD0CA74B3FF8D250A4283 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 1A3DE4B0B684E0679C64523E /* Pods-Runner.debug.xcconfig */,
+ C4C62015C2EE2797ABF091BA /* Pods-Runner.release.xcconfig */,
+ 1653BF8CF7D70B08243DF6A2 /* Pods-Runner.profile.xcconfig */,
+ );
+ name = Pods;
+ path = Pods;
+ sourceTree = "<group>";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "<group>";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 8E5CD0CA74B3FF8D250A4283 /* Pods */,
+ 2BE839568BFB70043910F5F6 /* Frameworks */,
+ );
+ sourceTree = "<group>";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ );
+ path = Runner;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ CAB15821656924BCC3058D97 /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ DBD650350FBE96B42D816E77 /* [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 = "";
+ TargetAttributes = {
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ 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";
+ };
+ CAB15821656924BCC3058D97 /* [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;
+ };
+ DBD650350FBE96B42D816E77 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "<group>";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+/* 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 = 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)";
+ DEVELOPMENT_TEAM = S8QB4VV633;
+ 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.smiley;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 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_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_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = S8QB4VV633;
+ 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.smiley;
+ 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)";
+ DEVELOPMENT_TEAM = S8QB4VV633;
+ 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.smiley;
+ 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/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "group:Runner.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IDEDidComputeMac32BitWarning</key>
+ <true/>
+</dict>
+</plist>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>PreviewsEnabled</key>
+ <false/>
+</dict>
+</plist>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..a28140c
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "1020"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Profile"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..21a3cc1
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "group:Runner.xcodeproj">
+ </FileRef>
+ <FileRef
+ location = "group:Pods/Pods.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IDEDidComputeMac32BitWarning</key>
+ <true/>
+</dict>
+</plist>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>PreviewsEnabled</key>
+ <false/>
+</dict>
+</plist>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/AppDelegate.swift b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..70693e4
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+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/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..dc9ada4
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..28c6bf0
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..f091b6b
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cde121
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..d0ef06e
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..dcdc230
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..c8f9ed8
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..75b2d16
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..c4df70d
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..6a84f41
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..d0e1f58
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
+ <viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
+ </imageView>
+ </subviews>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ <constraints>
+ <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
+ <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
+ </constraints>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+ <resources>
+ <image name="LaunchImage" width="168" height="185"/>
+ </resources>
+</document>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Base.lproj/Main.storyboard b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ </dependencies>
+ <scenes>
+ <!--Flutter View Controller-->
+ <scene sceneID="tne-QT-ifu">
+ <objects>
+ <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+ <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+ </objects>
+ </scene>
+ </scenes>
+</document>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Info.plist b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Info.plist
new file mode 100644
index 0000000..9907fcd
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>smiley</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>$(FLUTTER_BUILD_NAME)</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>$(FLUTTER_BUILD_NUMBER)</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UIViewControllerBasedStatusBarAppearance</key>
+ <false/>
+</dict>
+</plist>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Runner-Bridging-Header.h b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/lib/main.dart b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/lib/main.dart
new file mode 100644
index 0000000..adec8c1
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/lib/main.dart
@@ -0,0 +1,116 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert' show jsonEncode;
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:http/http.dart' as http;
+import 'package:poll_ios_stats/poll_ios_stats.dart';
+
+String _hostIp;
+
+Future<String> _getHostIp() async {
+ return rootBundle.loadString('assets/ip.txt');
+}
+
+Future<void> _sendResult(double result) async {
+ assert(_hostIp != null);
+ print('sending result $result...');
+ final String measurementName =
+ '${Platform.isAndroid ? "android_" : "ios_"}startup_time';
+ final http.Response response = await http.post(
+ 'http://$_hostIp',
+ headers: <String, String>{
+ 'Content-Type': 'application/json; charset=UTF-8',
+ },
+ body: jsonEncode(<String, dynamic>{
+ 'test': 'smiley',
+ 'platform': 'flutter',
+ 'results': <String, double>{measurementName: result},
+ }),
+ );
+ if (response.statusCode != 200) {
+ print('error when posting results:${response.statusCode}');
+ }
+}
+
+void main() {
+ runApp(const MyApp());
+ // Hide status bar.
+ SystemChrome.setEnabledSystemUIOverlays(<SystemUiOverlay>[]);
+ _getHostIp().then((String ip) async {
+ _hostIp = ip;
+ });
+}
+
+/// Top level Material App.
+class MyApp extends StatelessWidget {
+ /// Create top level Material App.
+ const MyApp({Key key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'Flutter Demo',
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
+ visualDensity: VisualDensity.adaptivePlatformDensity,
+ ),
+ home: const MyHomePage(),
+ );
+ }
+}
+
+/// `Scaffold` that has the image widget.
+class MyHomePage extends StatefulWidget {
+ /// Standard constructor for a StatefulWidget.
+ const MyHomePage({Key key}) : super(key: key);
+
+ @override
+ _MyHomePageState createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+ final AssetImage _image = const AssetImage('images/smiley.png');
+ bool _loading = true;
+
+ @override
+ void initState() {
+ _image
+ .resolve(const ImageConfiguration())
+ .addListener(ImageStreamListener((_, __) {
+ if (mounted) {
+ setState(() {
+ _loading = false;
+ // This should get called when the image has actually been drawn to the screen.
+ WidgetsBinding.instance.addPostFrameCallback((_) async {
+ final DateTime renderTime = DateTime.now();
+ final PollIosStats _poller = PollIosStats();
+ final StartupTime startupTime = await _poller.pollStartupTime();
+ final Duration diff = renderTime.difference(
+ DateTime.fromMicrosecondsSinceEpoch(startupTime.startupTime));
+ _sendResult(diff.inMicroseconds / 1000000.0);
+ });
+ });
+ }
+ }));
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ if (_loading) Container() else Image(image: _image),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/pubspec.yaml b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/pubspec.yaml
new file mode 100644
index 0000000..61a4149
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/pubspec.yaml
@@ -0,0 +1,74 @@
+name: smiley
+description: A new Flutter project.
+
+# The following line prevents the package from being accidentally published to
+# pub.dev using `pub publish`. This is preferred for private packages.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+
+# The following defines the version and build number for your application.
+# A version number is three numbers separated by dots, like 1.2.43
+# followed by an optional build number separated by a +.
+# Both the version and the builder number may be overridden in flutter
+# build by specifying --build-name and --build-number, respectively.
+# In Android, build-name is used as versionName while build-number used as versionCode.
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
+# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+version: 1.0.0+1
+
+environment:
+ sdk: ">=2.7.0 <3.0.0"
+
+dependencies:
+ cupertino_icons: ^0.1.3
+ flutter:
+ sdk: flutter
+ http: ^0.12.2
+ poll_ios_stats: ^0.0.1
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+# 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/smiley.png
+ - assets/ip.txt
+
+ # 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/imitation_game/imitation_tests/smiley/uikit/run_ios.sh b/packages/imitation_game/imitation_tests/smiley/uikit/run_ios.sh
new file mode 100755
index 0000000..794db72
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/run_ios.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+# TODO: What about customizing the DEVELOPMENT_TEAM?
+set -e
+cd $( dirname "${BASH_SOURCE[0]}" )
+cd smiley
+xcodebuild -scheme smiley -configuration Release -project smiley.xcodeproj -archivePath ./smiley.xcarchive archive
+xcodebuild -exportArchive -archivePath ./smiley.xcarchive/ -exportPath . -exportOptionsPlist exportOptions.plist -allowProvisioningUpdates
+ios-deploy --justlaunch --bundle ./smiley.xcarchive/Products/Applications/smiley.app
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/exportOptions.plist b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/exportOptions.plist
new file mode 100644
index 0000000..687b403
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/exportOptions.plist
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>compileBitcode</key>
+ <true/>
+ <key>method</key>
+ <string>development</string>
+ <key>signingStyle</key>
+ <string>automatic</string>
+ <key>stripSwiftSymbols</key>
+ <true/>
+ <key>teamID</key>
+ <string>S8QB4VV633</string>
+ <key>thinning</key>
+ <string><none></string>
+</dict>
+</plist>
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.pbxproj b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..aca7bdb
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.pbxproj
@@ -0,0 +1,350 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 50;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 0D313B6D24C6613900D7044B /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D313B6C24C6613900D7044B /* AppDelegate.m */; };
+ 0D313B7024C6613900D7044B /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D313B6F24C6613900D7044B /* SceneDelegate.m */; };
+ 0D313B7324C6613900D7044B /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D313B7224C6613900D7044B /* ViewController.m */; };
+ 0D313B7624C6613900D7044B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0D313B7424C6613900D7044B /* Main.storyboard */; };
+ 0D313B7824C6613A00D7044B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0D313B7724C6613A00D7044B /* Assets.xcassets */; };
+ 0D313B7B24C6613A00D7044B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0D313B7924C6613A00D7044B /* LaunchScreen.storyboard */; };
+ 0D313B7E24C6613A00D7044B /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D313B7D24C6613A00D7044B /* main.m */; };
+ 0D313B8524C66AB700D7044B /* ip.txt in Resources */ = {isa = PBXBuildFile; fileRef = 0D313B8424C66AB700D7044B /* ip.txt */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 0D313B6824C6613900D7044B /* smiley.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = smiley.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 0D313B6B24C6613900D7044B /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
+ 0D313B6C24C6613900D7044B /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+ 0D313B6E24C6613900D7044B /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = "<group>"; };
+ 0D313B6F24C6613900D7044B /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = "<group>"; };
+ 0D313B7124C6613900D7044B /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = "<group>"; };
+ 0D313B7224C6613900D7044B /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = "<group>"; };
+ 0D313B7524C6613900D7044B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+ 0D313B7724C6613A00D7044B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+ 0D313B7A24C6613A00D7044B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+ 0D313B7C24C6613A00D7044B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ 0D313B7D24C6613A00D7044B /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+ 0D313B8424C66AB700D7044B /* ip.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ip.txt; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 0D313B6524C6613900D7044B /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 0D313B5F24C6613900D7044B = {
+ isa = PBXGroup;
+ children = (
+ 0D313B6A24C6613900D7044B /* smiley */,
+ 0D313B6924C6613900D7044B /* Products */,
+ );
+ sourceTree = "<group>";
+ };
+ 0D313B6924C6613900D7044B /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 0D313B6824C6613900D7044B /* smiley.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 0D313B6A24C6613900D7044B /* smiley */ = {
+ isa = PBXGroup;
+ children = (
+ 0D313B6B24C6613900D7044B /* AppDelegate.h */,
+ 0D313B6C24C6613900D7044B /* AppDelegate.m */,
+ 0D313B6E24C6613900D7044B /* SceneDelegate.h */,
+ 0D313B6F24C6613900D7044B /* SceneDelegate.m */,
+ 0D313B7124C6613900D7044B /* ViewController.h */,
+ 0D313B7224C6613900D7044B /* ViewController.m */,
+ 0D313B7424C6613900D7044B /* Main.storyboard */,
+ 0D313B7724C6613A00D7044B /* Assets.xcassets */,
+ 0D313B7924C6613A00D7044B /* LaunchScreen.storyboard */,
+ 0D313B7C24C6613A00D7044B /* Info.plist */,
+ 0D313B7D24C6613A00D7044B /* main.m */,
+ 0D313B8424C66AB700D7044B /* ip.txt */,
+ );
+ path = smiley;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 0D313B6724C6613900D7044B /* smiley */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 0D313B8124C6613A00D7044B /* Build configuration list for PBXNativeTarget "smiley" */;
+ buildPhases = (
+ 0D313B6424C6613900D7044B /* Sources */,
+ 0D313B6524C6613900D7044B /* Frameworks */,
+ 0D313B6624C6613900D7044B /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = smiley;
+ productName = smiley;
+ productReference = 0D313B6824C6613900D7044B /* smiley.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 0D313B6024C6613900D7044B /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1150;
+ ORGANIZATIONNAME = "Aaron Clarke";
+ TargetAttributes = {
+ 0D313B6724C6613900D7044B = {
+ CreatedOnToolsVersion = 11.5;
+ };
+ };
+ };
+ buildConfigurationList = 0D313B6324C6613900D7044B /* Build configuration list for PBXProject "smiley" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 0D313B5F24C6613900D7044B;
+ productRefGroup = 0D313B6924C6613900D7044B /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 0D313B6724C6613900D7044B /* smiley */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 0D313B6624C6613900D7044B /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0D313B7B24C6613A00D7044B /* LaunchScreen.storyboard in Resources */,
+ 0D313B8524C66AB700D7044B /* ip.txt in Resources */,
+ 0D313B7824C6613A00D7044B /* Assets.xcassets in Resources */,
+ 0D313B7624C6613900D7044B /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 0D313B6424C6613900D7044B /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0D313B7324C6613900D7044B /* ViewController.m in Sources */,
+ 0D313B6D24C6613900D7044B /* AppDelegate.m in Sources */,
+ 0D313B7E24C6613A00D7044B /* main.m in Sources */,
+ 0D313B7024C6613900D7044B /* SceneDelegate.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 0D313B7424C6613900D7044B /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 0D313B7524C6613900D7044B /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "<group>";
+ };
+ 0D313B7924C6613A00D7044B /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 0D313B7A24C6613A00D7044B /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 0D313B7F24C6613A00D7044B /* Debug */ = {
+ isa = XCBuildConfiguration;
+ 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_ENABLE_OBJC_WEAK = 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_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_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_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ 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_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.5;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ };
+ name = Debug;
+ };
+ 0D313B8024C6613A00D7044B /* Release */ = {
+ isa = XCBuildConfiguration;
+ 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_ENABLE_OBJC_WEAK = 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_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_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_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ 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_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.5;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 0D313B8224C6613A00D7044B /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_TEAM = S8QB4VV633;
+ INFOPLIST_FILE = smiley/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "dev.flutter.imitation-game.smiley";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 0D313B8324C6613A00D7044B /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_TEAM = S8QB4VV633;
+ INFOPLIST_FILE = smiley/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "dev.flutter.imitation-game.smiley";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 0D313B6324C6613900D7044B /* Build configuration list for PBXProject "smiley" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 0D313B7F24C6613A00D7044B /* Debug */,
+ 0D313B8024C6613A00D7044B /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 0D313B8124C6613A00D7044B /* Build configuration list for PBXNativeTarget "smiley" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 0D313B8224C6613A00D7044B /* Debug */,
+ 0D313B8324C6613A00D7044B /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 0D313B6024C6613900D7044B /* Project object */;
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1ba2d6e
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "self:smiley.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IDEDidComputeMac32BitWarning</key>
+ <true/>
+</dict>
+</plist>
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/AppDelegate.h b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/AppDelegate.h
new file mode 100644
index 0000000..9a7d7f2
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/AppDelegate.h
@@ -0,0 +1,9 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <UIKit/UIKit.h>
+
+@interface AppDelegate : UIResponder<UIApplicationDelegate>
+
+@end
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/AppDelegate.m b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/AppDelegate.m
new file mode 100644
index 0000000..cac844f
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/AppDelegate.m
@@ -0,0 +1,38 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "AppDelegate.h"
+
+@interface AppDelegate ()
+
+@end
+
+@implementation AppDelegate
+
+- (BOOL)application:(UIApplication *)application
+ didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+ // Override point for customization after application launch.
+ return YES;
+}
+
+#pragma mark - UISceneSession lifecycle
+
+- (UISceneConfiguration *)application:(UIApplication *)application
+ configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession
+ options:(UISceneConnectionOptions *)options {
+ // Called when a new scene session is being created.
+ // Use this method to select a configuration to create the new scene with.
+ return [[UISceneConfiguration alloc] initWithName:@"Default Configuration"
+ sessionRole:connectingSceneSession.role];
+}
+
+- (void)application:(UIApplication *)application
+ didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions {
+ // Called when the user discards a scene session.
+ // If any sessions were discarded while the application was not running, this will be called
+ // shortly after application:didFinishLaunchingWithOptions. Use this method to release any
+ // resources that were specific to the discarded scenes, as they will not return.
+}
+
+@end
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..9221b9bb
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,98 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "60x60"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "60x60"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "76x76"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "76x76"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "83.5x83.5"
+ },
+ {
+ "idiom" : "ios-marketing",
+ "scale" : "1x",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/Contents.json b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/smiley.imageset/Contents.json b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/smiley.imageset/Contents.json
new file mode 100644
index 0000000..79308c1
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/smiley.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "smiley.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/smiley.imageset/smiley.png b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/smiley.imageset/smiley.png
new file mode 100644
index 0000000..441b66a
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/smiley.imageset/smiley.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Base.lproj/LaunchScreen.storyboard b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..865e932
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
+ <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
+ <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Base.lproj/Main.storyboard b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..4f0a58a
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Base.lproj/Main.storyboard
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
+ <device id="retina6_1" orientation="portrait" appearance="light"/>
+ <dependencies>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
+ <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="tne-QT-ifu">
+ <objects>
+ <viewController id="BYZ-38-t0r" customClass="ViewController" sceneMemberID="viewController">
+ <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+ <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="lef-PC-56A">
+ <rect key="frame" x="0.0" y="241" width="414" height="414"/>
+ <constraints>
+ <constraint firstAttribute="width" secondItem="lef-PC-56A" secondAttribute="height" multiplier="1:1" id="f3c-4F-4K7"/>
+ </constraints>
+ </imageView>
+ </subviews>
+ <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
+ <constraints>
+ <constraint firstItem="lef-PC-56A" firstAttribute="trailing" secondItem="6Tk-OE-BBY" secondAttribute="trailing" id="Pf2-cI-sL8"/>
+ <constraint firstItem="lef-PC-56A" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="WPF-Fb-Wds"/>
+ <constraint firstItem="lef-PC-56A" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="WXd-85-JCf"/>
+ </constraints>
+ <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
+ </view>
+ <connections>
+ <outlet property="imageView" destination="lef-PC-56A" id="rfq-xo-kDP"/>
+ </connections>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="131.8840579710145" y="138.61607142857142"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Info.plist b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Info.plist
new file mode 100644
index 0000000..7b6037c
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Info.plist
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UIApplicationSceneManifest</key>
+ <dict>
+ <key>UIApplicationSupportsMultipleScenes</key>
+ <false/>
+ <key>UISceneConfigurations</key>
+ <dict>
+ <key>UIWindowSceneSessionRoleApplication</key>
+ <array>
+ <dict>
+ <key>UISceneConfigurationName</key>
+ <string>Default Configuration</string>
+ <key>UISceneDelegateClassName</key>
+ <string>SceneDelegate</string>
+ <key>UISceneStoryboardFile</key>
+ <string>Main</string>
+ </dict>
+ </array>
+ </dict>
+ </dict>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+</dict>
+</plist>
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/SceneDelegate.h b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/SceneDelegate.h
new file mode 100644
index 0000000..bc46a07
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/SceneDelegate.h
@@ -0,0 +1,11 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <UIKit/UIKit.h>
+
+@interface SceneDelegate : UIResponder<UIWindowSceneDelegate>
+
+@property(strong, nonatomic) UIWindow* window;
+
+@end
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/SceneDelegate.m b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/SceneDelegate.m
new file mode 100644
index 0000000..061f723
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/SceneDelegate.m
@@ -0,0 +1,52 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "SceneDelegate.h"
+
+@interface SceneDelegate ()
+
+@end
+
+@implementation SceneDelegate
+
+- (void)scene:(UIScene *)scene
+ willConnectToSession:(UISceneSession *)session
+ options:(UISceneConnectionOptions *)connectionOptions {
+ // Use this method to optionally configure and attach the UIWindow `window` to the provided
+ // UIWindowScene `scene`. If using a storyboard, the `window` property will automatically be
+ // initialized and attached to the scene. This delegate does not imply the connecting scene or
+ // session are new (see `application:configurationForConnectingSceneSession` instead).
+}
+
+- (void)sceneDidDisconnect:(UIScene *)scene {
+ // Called as the scene is being released by the system.
+ // This occurs shortly after the scene enters the background, or when its session is discarded.
+ // Release any resources associated with this scene that can be re-created the next time the scene
+ // connects. The scene may re-connect later, as its session was not neccessarily discarded (see
+ // `application:didDiscardSceneSessions` instead).
+}
+
+- (void)sceneDidBecomeActive:(UIScene *)scene {
+ // Called when the scene has moved from an inactive state to an active state.
+ // Use this method to restart any tasks that were paused (or not yet started) when the scene was
+ // inactive.
+}
+
+- (void)sceneWillResignActive:(UIScene *)scene {
+ // Called when the scene will move from an active state to an inactive state.
+ // This may occur due to temporary interruptions (ex. an incoming phone call).
+}
+
+- (void)sceneWillEnterForeground:(UIScene *)scene {
+ // Called as the scene transitions from the background to the foreground.
+ // Use this method to undo the changes made on entering the background.
+}
+
+- (void)sceneDidEnterBackground:(UIScene *)scene {
+ // Called as the scene transitions from the foreground to the background.
+ // Use this method to save data, release shared resources, and store enough scene-specific state
+ // information to restore the scene back to its current state.
+}
+
+@end
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ViewController.h b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ViewController.h
new file mode 100644
index 0000000..a3585bf
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ViewController.h
@@ -0,0 +1,10 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <UIKit/UIKit.h>
+
+@interface ViewController : UIViewController
+@property(nonatomic, strong) IBOutlet UIImageView* imageView;
+
+@end
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ViewController.m b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ViewController.m
new file mode 100644
index 0000000..46596b7
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ViewController.m
@@ -0,0 +1,114 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ViewController.h"
+#import <mach/mach.h>
+#import <sys/sysctl.h>
+#import <sys/types.h>
+
+static int64_t loadStartupTime(NSError **error) {
+ pid_t pid = [[NSProcessInfo processInfo] processIdentifier];
+ int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
+ struct kinfo_proc proc;
+ size_t size = sizeof(proc);
+ int err = sysctl(mib, 4, &proc, &size, NULL, 0);
+ if (err != 0) {
+ int errCode = errno;
+ if (error) {
+ *error = [NSError errorWithDomain:@"smiley" code:errCode userInfo:@{}];
+ }
+ return 0;
+ }
+
+ struct timeval startTime = proc.kp_proc.p_starttime;
+ int64_t microsecondsInSecond = 1000000LL;
+ int64_t microsecondsSinceEpoch =
+ (int64_t)(startTime.tv_sec * microsecondsInSecond) + (int64_t)startTime.tv_usec;
+
+ return microsecondsSinceEpoch;
+}
+
+static NSString *loadIpAddress() {
+#if TARGET_IPHONE_SIMULATOR
+ return @"127.0.0.1:4040";
+#else
+ NSString *ipPath = [[NSBundle mainBundle] pathForResource:@"ip" ofType:@"txt"];
+ NSError *err;
+ NSString *ipAddress =
+ [NSString stringWithContentsOfFile:ipPath encoding:NSUTF8StringEncoding error:&err];
+ assert(err == nil);
+ return
+ [ipAddress stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
+#endif
+}
+
+static void sendResults(NSTimeInterval result) {
+ NSLog(@"send results:%f", result);
+ NSString *ipAddress = loadIpAddress();
+ NSString *url = [NSString stringWithFormat:@"http://%@", ipAddress];
+ NSMutableURLRequest *urlRequest =
+ [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
+ NSDictionary *payload = @{
+ @"test" : @"smiley",
+ @"platform" : @"uikit",
+ @"results" : @{
+ @"ios_startup_time" : @(result),
+ }
+ };
+ NSError *error;
+ NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error];
+ assert(error == nil && jsonData);
+ [urlRequest setHTTPMethod:@"POST"];
+ [urlRequest setHTTPBody:jsonData];
+
+ NSURLSession *session = [NSURLSession sharedSession];
+ NSURLSessionDataTask *dataTask =
+ [session dataTaskWithRequest:urlRequest
+ completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
+ NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
+ if (httpResponse.statusCode != 200) {
+ NSLog(@"Error %@ '%@'", error, url);
+ }
+ }];
+ [dataTask resume];
+}
+
+@interface ViewController ()
+@property(nonatomic, strong) CADisplayLink *displayLink;
+@end
+
+@implementation ViewController {
+ BOOL _waitingForDraw;
+}
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+}
+
+- (void)loadView {
+ [super loadView];
+ self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onTick:)];
+ [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
+ self.imageView.image = [UIImage imageNamed:@"smiley"];
+ _waitingForDraw = YES;
+}
+
+- (BOOL)prefersStatusBarHidden {
+ return YES;
+}
+
+- (void)onTick:(CADisplayLink *)sender {
+ if (_waitingForDraw) {
+ int64_t epochTime = loadStartupTime(nil);
+ NSTimeInterval epocTimeSeconds = (double)epochTime / 1000000.0;
+ NSDate *startTime = [NSDate dateWithTimeIntervalSince1970:epocTimeSeconds];
+ NSTimeInterval runTime = [[NSDate now] timeIntervalSinceDate:startTime];
+ sendResults(runTime);
+ _waitingForDraw = NO;
+ [self.displayLink invalidate];
+ self.displayLink = nil;
+ }
+}
+
+@end
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ip.txt b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ip.txt
new file mode 100644
index 0000000..d331e21
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ip.txt
@@ -0,0 +1 @@
+192.168.0.18:4040
\ No newline at end of file
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/main.m b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/main.m
new file mode 100644
index 0000000..9098614
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/main.m
@@ -0,0 +1,15 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <UIKit/UIKit.h>
+#import "AppDelegate.h"
+
+int main(int argc, char* argv[]) {
+ NSString* appDelegateClassName;
+ @autoreleasepool {
+ // Setup code that might create autoreleased objects goes here.
+ appDelegateClassName = NSStringFromClass([AppDelegate class]);
+ }
+ return UIApplicationMain(argc, argv, nil, appDelegateClassName);
+}
diff --git a/packages/imitation_game/last_results.json b/packages/imitation_game/last_results.json
new file mode 100644
index 0000000..5344819
--- /dev/null
+++ b/packages/imitation_game/last_results.json
@@ -0,0 +1,14 @@
+{
+ "smiley": {
+ "flutter": {
+ "ios_startup_time": 0.691717,
+ "adb_memory_total": 43179.0
+ },
+ "uikit": {
+ "ios_startup_time": 0.2632870674133301
+ },
+ "android": {
+ "adb_memory_total": 97941.0
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/imitation_game/lib/imitation_game.dart b/packages/imitation_game/lib/imitation_game.dart
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/packages/imitation_game/lib/imitation_game.dart
@@ -0,0 +1 @@
+
diff --git a/packages/imitation_game/lib/readme_template.dart b/packages/imitation_game/lib/readme_template.dart
new file mode 100644
index 0000000..fc69cfb
--- /dev/null
+++ b/packages/imitation_game/lib/readme_template.dart
@@ -0,0 +1,111 @@
+/// Mustache template used for generating README.md.
+String readmeTemplate = """
+# Imitation Game
+
+## Description
+
+`imitation_game` is a platform for performing automated tests to compare the
+performance of different UI frameworks. For example, how much memory does the
+same app written in Flutter and UIKit take?
+
+## Running all the tests
+
+You need a mobile device plugged into your computer and setup for development.
+The mobile device and the computer need to be on the same network, one that
+allows communication between computers since that's how the mobile phone will
+report its results to the computer.
+
+```sh
+dart imitation_game.dart
+```
+
+## Dependencies
+
+In order to run the tests you will need the union of all the platforms being
+tested. As new tests are added please add to this list:
+
+### iOS
+
+- Flutter
+- Xcode
+- [ios_deploy](https://github.com/ios-control/ios-deploy) - used to launch apps
+ on the attached iOS device.
+
+## Example File Layout
+
+```text
+./
+├─ imitation_game.dart
+└─ imitation_tests/
+ ├─ smiley/
+ │ ├─ README.md
+ │ ├─ flutter/
+ │ │ ├─ run_ios.sh
+ │ │ └─ <flutter project files>
+ │ └─ uikit/
+ │ ├─ run_ios.sh
+ │ └─ <uikit project files>
+ └─ memory/
+ ├─ README.md
+ ├─ flutter/
+ │ ├─ run_ios.sh
+ │ └─ <flutter project files>
+ └─ uikit/
+ ├─ run_ios.sh
+ └─ <uikit project files>
+```
+
+Here there are 2 different tests with 2 different platform implementations. The
+tests are named `smiley` and `memory`, they are both implemented on the
+platforms `flutter` and `uikit`.
+
+### Adding a test
+
+Tests should comprise of implementations on one or more platform. The directory
+for the test should be added to `./tests`. Inside that directory there should
+be a directory of implementations and a `README.md` file that explains the test.
+
+### Adding an implementation to a test
+
+An implementation has to follow these rules:
+
+- It needs to perform the same operations as the other implementations and
+ follow the description in the test's `README.md`.
+- It needs to contain a `run_ios.sh` script that will build and launch the test
+ on the connected device.
+- It should contain a file named `ip.txt` which will be overwritten by
+ `imitation_game.dart` with the ip address and port that should be used to
+ report results to.
+- It needs to report its results to the ip and port in the `ip.txt` via an HTTP
+ POST of JSON data.
+
+## Data format for results
+
+```json
+{
+ "test": "name_of_test",
+ "platform": "name_of_platform",
+ "results": {
+ "some_result_name": 1.23,
+ "some_result_name2": 4.56,
+ }
+}
+```
+
+A single test run can report multiple numbers.
+
+## Results
+Date created: {{date}}
+{{flutterVersion}}
+
+{{#tests}}
+- {{name}}
+ {{#platforms}}
+ - {{name}}
+ {{#measurements}}
+ - {{name}}: {{value}}
+ {{/measurements}}
+ {{/platforms}}
+{{/tests}}
+
+""";
diff --git a/packages/imitation_game/pubspec.yaml b/packages/imitation_game/pubspec.yaml
new file mode 100644
index 0000000..fea3802
--- /dev/null
+++ b/packages/imitation_game/pubspec.yaml
@@ -0,0 +1,9 @@
+name: imitation_game
+version: 0.0.1
+description: Testing framework for comparing multiple frameworks' performance.
+homepage: https://github.com/flutter/packages/tree/master/packages/imitation_game
+dependencies:
+ args: ^1.6.0
+ mustache: ^1.1.1
+environment:
+ sdk: ">=2.7.0 <3.0.0"
diff --git a/packages/imitation_game/run.sh b/packages/imitation_game/run.sh
new file mode 100755
index 0000000..5107632
--- /dev/null
+++ b/packages/imitation_game/run.sh
@@ -0,0 +1,5 @@
+if [ $# -eq 0 ]; then
+ echo "usage: run.sh [android | ios]"
+fi
+
+pub run imitation_game --platform=$1
diff --git a/packages/metrics_center/.gitignore b/packages/metrics_center/.gitignore
new file mode 100644
index 0000000..d97c5ea
--- /dev/null
+++ b/packages/metrics_center/.gitignore
@@ -0,0 +1 @@
+secret
diff --git a/packages/metrics_center/CHANGELOG.md b/packages/metrics_center/CHANGELOG.md
new file mode 100644
index 0000000..4e629f2
--- /dev/null
+++ b/packages/metrics_center/CHANGELOG.md
@@ -0,0 +1,29 @@
+# 0.1.0
+
+- `update` now requires DateTime when commit was merged
+- Removed `github` dependency
+
+# 0.0.9
+
+- Remove legacy datastore and destination.
+
+# 0.0.8
+
+- Allow tests to override LegacyFlutterDestination GCP project id.
+
+# 0.0.7
+
+- Expose constants that were missing since 0.0.4+1.
+
+# 0.0.6
+
+- Allow `datastoreFromCredentialsJson` to specify project id.
+
+# 0.0.5
+
+- `FlutterDestination` writes into both Skia perf GCS and the legacy datastore.
+- `FlutterDestination.makeFromAccessToken` returns a `Future`.
+
+# 0.0.4+1
+
+- Moved to the `flutter/packages` repository
diff --git a/packages/metrics_center/LICENSE b/packages/metrics_center/LICENSE
new file mode 100644
index 0000000..922fc0c
--- /dev/null
+++ b/packages/metrics_center/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2014 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.
\ No newline at end of file
diff --git a/packages/metrics_center/README.md b/packages/metrics_center/README.md
new file mode 100644
index 0000000..8f1418d
--- /dev/null
+++ b/packages/metrics_center/README.md
@@ -0,0 +1,9 @@
+# Metrics Center
+
+Metrics center is a minimal set of code and services to support multiple perf
+metrics generators (e.g., Cocoon device lab, Cirrus bots, LUCI bots, Firebase
+Test Lab) and destinations (e.g., old Cocoon perf dashboard, Skia perf
+dashboard). The work and maintenance it requires is very close to that of just
+supporting a single generator and destination (e.g., engine bots to Skia perf),
+and the small amount of extra work is designed to make it easy to support more
+generators and destinations in the future.
diff --git a/packages/metrics_center/lib/metrics_center.dart b/packages/metrics_center/lib/metrics_center.dart
new file mode 100644
index 0000000..3790116
--- /dev/null
+++ b/packages/metrics_center/lib/metrics_center.dart
@@ -0,0 +1,9 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+export 'src/common.dart';
+export 'src/constants.dart';
+export 'src/flutter.dart';
+export 'src/google_benchmark.dart';
+export 'src/skiaperf.dart';
diff --git a/packages/metrics_center/lib/src/common.dart b/packages/metrics_center/lib/src/common.dart
new file mode 100644
index 0000000..ee17c40
--- /dev/null
+++ b/packages/metrics_center/lib/src/common.dart
@@ -0,0 +1,65 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:collection';
+import 'dart:convert';
+
+import 'package:crypto/crypto.dart';
+import 'package:equatable/equatable.dart';
+
+import 'package:googleapis_auth/auth.dart';
+import 'package:googleapis_auth/auth_io.dart';
+import 'package:http/http.dart';
+
+/// Common format of a metric data point.
+class MetricPoint extends Equatable {
+ /// Creates a new data point.
+ MetricPoint(
+ this.value,
+ Map<String, String> tags,
+ ) : _tags = SplayTreeMap<String, String>.from(tags);
+
+ /// Can store integer values.
+ final double value;
+
+ /// Test name, unit, timestamp, configs, git revision, ..., in sorted order.
+ UnmodifiableMapView<String, String> get tags =>
+ UnmodifiableMapView<String, String>(_tags);
+
+ /// Unique identifier for updating existing data point.
+ ///
+ /// We shouldn't have to worry about hash collisions until we have about
+ /// 2^128 points.
+ ///
+ /// This id should stay constant even if the [tags.keys] are reordered.
+ /// (Because we are using an ordered SplayTreeMap to generate the id.)
+ String get id => sha256.convert(utf8.encode('$_tags')).toString();
+
+ @override
+ String toString() {
+ return 'MetricPoint(value=$value, tags=$_tags)';
+ }
+
+ final SplayTreeMap<String, String> _tags;
+
+ @override
+ List<Object> get props => <Object>[value, tags];
+}
+
+/// Interface to write [MetricPoint].
+abstract class MetricDestination {
+ /// Insert new data points or modify old ones with matching id.
+ Future<void> update(List<MetricPoint> points, DateTime commitTime);
+}
+
+/// Create `AuthClient` in case we only have an access token without the full
+/// credentials json. It's currently the case for Chrmoium LUCI bots.
+AuthClient authClientFromAccessToken(String token, List<String> scopes) {
+ final DateTime anHourLater = DateTime.now().add(const Duration(hours: 1));
+ final AccessToken accessToken =
+ AccessToken('Bearer', token, anHourLater.toUtc());
+ final AccessCredentials accessCredentials =
+ AccessCredentials(accessToken, null, scopes);
+ return authenticatedClient(Client(), accessCredentials);
+}
diff --git a/packages/metrics_center/lib/src/constants.dart b/packages/metrics_center/lib/src/constants.dart
new file mode 100644
index 0000000..49c8244
--- /dev/null
+++ b/packages/metrics_center/lib/src/constants.dart
@@ -0,0 +1,51 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Strings used for MetricPoint tag keys
+const String kGithubRepoKey = 'gitRepo';
+
+/// Strings used for MetricPoint tag keys
+const String kGitRevisionKey = 'gitRevision';
+
+/// Strings used for MetricPoint tag keys
+const String kUnitKey = 'unit';
+
+/// Strings used for MetricPoint tag keys
+const String kNameKey = 'name';
+
+/// Strings used for MetricPoint tag keys
+const String kSubResultKey = 'subResult';
+
+/// Flutter repo name.
+const String kFlutterFrameworkRepo = 'flutter/flutter';
+
+/// Engine repo name.
+const String kFlutterEngineRepo = 'flutter/engine';
+
+/// The key for the GCP project id in the credentials json.
+const String kProjectId = 'project_id';
+
+/// Timeline key in JSON.
+const String kSourceTimeMicrosName = 'sourceTimeMicros';
+
+/// The prod bucket name for Flutter's instance of Skia Perf.
+const String kBucketName = 'flutter-skia-perf-prod';
+
+/// The test bucket name for Flutter's instance of Skia Perf.
+const String kTestBucketName = 'flutter-skia-perf-test';
+
+/// JSON key for Skia Perf's git hash entry.
+const String kSkiaPerfGitHashKey = 'gitHash';
+
+/// JSON key for Skia Perf's results entry.
+const String kSkiaPerfResultsKey = 'results';
+
+/// JSON key for Skia Perf's value entry.
+const String kSkiaPerfValueKey = 'value';
+
+/// JSON key for Skia Perf's options entry.
+const String kSkiaPerfOptionsKey = 'options';
+
+/// JSON key for Skia Perf's default config entry.
+const String kSkiaPerfDefaultConfig = 'default';
diff --git a/packages/metrics_center/lib/src/flutter.dart b/packages/metrics_center/lib/src/flutter.dart
new file mode 100644
index 0000000..89e35ae
--- /dev/null
+++ b/packages/metrics_center/lib/src/flutter.dart
@@ -0,0 +1,60 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'common.dart';
+import 'constants.dart';
+import 'skiaperf.dart';
+
+/// Convenient class to capture the benchmarks in the Flutter engine repo.
+class FlutterEngineMetricPoint extends MetricPoint {
+ /// Creates a metric point for the Flutter engine repository.
+ ///
+ /// The `name`, `value`, and `gitRevision` parameters must not be null.
+ FlutterEngineMetricPoint(
+ String name,
+ double value,
+ String gitRevision, {
+ Map<String, String> moreTags = const <String, String>{},
+ }) : super(
+ value,
+ <String, String>{
+ kNameKey: name,
+ kGithubRepoKey: kFlutterEngineRepo,
+ kGitRevisionKey: gitRevision,
+ }..addAll(moreTags),
+ );
+}
+
+/// All Flutter performance metrics (framework, engine, ...) should be written
+/// to this destination.
+class FlutterDestination extends MetricDestination {
+ FlutterDestination._(this._skiaPerfDestination);
+
+ /// Creates a [FlutterDestination] from service account JSON.
+ static Future<FlutterDestination> makeFromCredentialsJson(
+ Map<String, dynamic> json,
+ {bool isTesting = false}) async {
+ final SkiaPerfDestination skiaPerfDestination =
+ await SkiaPerfDestination.makeFromGcpCredentials(json,
+ isTesting: isTesting);
+ return FlutterDestination._(skiaPerfDestination);
+ }
+
+ /// Creates a [FlutterDestination] from an OAuth access token.
+ static Future<FlutterDestination> makeFromAccessToken(
+ String accessToken, String projectId,
+ {bool isTesting = false}) async {
+ final SkiaPerfDestination skiaPerfDestination =
+ await SkiaPerfDestination.makeFromAccessToken(accessToken, projectId,
+ isTesting: isTesting);
+ return FlutterDestination._(skiaPerfDestination);
+ }
+
+ @override
+ Future<void> update(List<MetricPoint> points, DateTime commitTime) async {
+ await _skiaPerfDestination.update(points, commitTime);
+ }
+
+ final SkiaPerfDestination _skiaPerfDestination;
+}
diff --git a/packages/metrics_center/lib/src/gcs_lock.dart b/packages/metrics_center/lib/src/gcs_lock.dart
new file mode 100644
index 0000000..47d0d23
--- /dev/null
+++ b/packages/metrics_center/lib/src/gcs_lock.dart
@@ -0,0 +1,91 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:googleapis/storage/v1.dart';
+import 'package:googleapis_auth/auth.dart';
+
+/// Global (in terms of earth) mutex using Google Cloud Storage.
+class GcsLock {
+ /// Create a lock with an authenticated client and a GCS bucket name.
+ ///
+ /// The client is used to communicate with Google Cloud Storage APIs.
+ GcsLock(this._client, this._bucketName)
+ : assert(_client != null),
+ assert(_bucketName != null) {
+ _api = StorageApi(_client);
+ }
+
+ /// Create a temporary lock file in GCS, and use it as a mutex mechanism to
+ /// run a piece of code exclusively.
+ ///
+ /// There must be no existing lock file with the same name in order to
+ /// proceed. If multiple [GcsLock]s with the same `bucketName` and
+ /// `lockFileName` try [protectedRun] simultaneously, only one will proceed
+ /// and create the lock file. All others will be blocked.
+ ///
+ /// When [protectedRun] finishes, the lock file is deleted, and other blocked
+ /// [protectedRun] may proceed.
+ ///
+ /// If the lock file is stuck (e.g., `_unlock` is interrupted unexpectedly),
+ /// one may need to manually delete the lock file from GCS to unblock any
+ /// [protectedRun] that may depend on it.
+ Future<void> protectedRun(
+ String lockFileName, Future<void> Function() f) async {
+ await _lock(lockFileName);
+ try {
+ await f();
+ } catch (e, stacktrace) {
+ print(stacktrace);
+ rethrow;
+ } finally {
+ await _unlock(lockFileName);
+ }
+ }
+
+ Future<void> _lock(String lockFileName) async {
+ final Object object = Object();
+ object.bucket = _bucketName;
+ object.name = lockFileName;
+ final Media content = Media(const Stream<List<int>>.empty(), 0);
+
+ Duration waitPeriod = const Duration(milliseconds: 10);
+ bool locked = false;
+ while (!locked) {
+ try {
+ await _api.objects.insert(object, _bucketName,
+ ifGenerationMatch: '0', uploadMedia: content);
+ locked = true;
+ } on DetailedApiRequestError catch (e) {
+ if (e.status == 412) {
+ // Status 412 means that the lock file already exists. Wait until
+ // that lock file is deleted.
+ await Future<void>.delayed(waitPeriod);
+ waitPeriod *= 2;
+ if (waitPeriod >= _kWarningThreshold) {
+ print(
+ 'The lock is waiting for a long time: $waitPeriod. '
+ 'If the lock file $lockFileName in bucket $_bucketName '
+ 'seems to be stuck (i.e., it was created a long time ago and '
+ 'no one seems to be owning it currently), delete it manually '
+ 'to unblock this.',
+ );
+ }
+ } else {
+ rethrow;
+ }
+ }
+ }
+ }
+
+ Future<void> _unlock(String lockFileName) async {
+ await _api.objects.delete(_bucketName, lockFileName);
+ }
+
+ StorageApi _api;
+
+ final String _bucketName;
+ final AuthClient _client;
+
+ static const Duration _kWarningThreshold = Duration(seconds: 10);
+}
diff --git a/packages/metrics_center/lib/src/google_benchmark.dart b/packages/metrics_center/lib/src/google_benchmark.dart
new file mode 100644
index 0000000..ab4ae22
--- /dev/null
+++ b/packages/metrics_center/lib/src/google_benchmark.dart
@@ -0,0 +1,74 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'common.dart';
+import 'constants.dart';
+
+const String _kTimeUnitKey = 'time_unit';
+
+const List<String> _kNonNumericalValueSubResults = <String>[
+ kNameKey,
+ _kTimeUnitKey,
+ 'iterations',
+ 'big_o',
+];
+
+/// Parse the json result of https://github.com/google/benchmark.
+class GoogleBenchmarkParser {
+ /// Given a Google benchmark json output, parse its content into a list of [MetricPoint].
+ static Future<List<MetricPoint>> parse(String jsonFileName) async {
+ final Map<String, dynamic> jsonResult =
+ jsonDecode(File(jsonFileName).readAsStringSync())
+ as Map<String, dynamic>;
+
+ final Map<String, dynamic> rawContext =
+ jsonResult['context'] as Map<String, dynamic>;
+ final Map<String, String> context = rawContext.map<String, String>(
+ (String k, dynamic v) => MapEntry<String, String>(k, v.toString()),
+ );
+ final List<MetricPoint> points = <MetricPoint>[];
+ for (final dynamic item in jsonResult['benchmarks']) {
+ _parseAnItem(item as Map<String, dynamic>, points, context);
+ }
+ return points;
+ }
+}
+
+void _parseAnItem(
+ Map<String, dynamic> item,
+ List<MetricPoint> points,
+ Map<String, String> context,
+) {
+ final String name = item[kNameKey] as String;
+ final Map<String, String> timeUnitMap = <String, String>{
+ kUnitKey: item[_kTimeUnitKey] as String
+ };
+ for (final String subResult in item.keys) {
+ if (!_kNonNumericalValueSubResults.contains(subResult)) {
+ num rawValue;
+ try {
+ rawValue = item[subResult] as num;
+ } catch (e) {
+ print(
+ '$subResult: ${item[subResult]} (${item[subResult].runtimeType}) is not a number');
+ rethrow;
+ }
+
+ final double value =
+ rawValue is int ? rawValue.toDouble() : rawValue as double;
+ points.add(
+ MetricPoint(
+ value,
+ <String, String>{kNameKey: name, kSubResultKey: subResult}
+ ..addAll(context)
+ ..addAll(
+ subResult.endsWith('time') ? timeUnitMap : <String, String>{}),
+ ),
+ );
+ }
+ }
+}
diff --git a/packages/metrics_center/lib/src/skiaperf.dart b/packages/metrics_center/lib/src/skiaperf.dart
new file mode 100644
index 0000000..2a9c4bd
--- /dev/null
+++ b/packages/metrics_center/lib/src/skiaperf.dart
@@ -0,0 +1,438 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:gcloud/storage.dart';
+import 'package:googleapis/storage/v1.dart' show DetailedApiRequestError;
+import 'package:googleapis_auth/auth.dart';
+import 'package:googleapis_auth/auth_io.dart';
+
+import 'common.dart';
+import 'constants.dart';
+import 'gcs_lock.dart';
+
+/// A [MetricPoint] modeled after the format that Skia Perf expects.
+///
+/// Skia Perf Format is a JSON file that looks like:
+/// ```json
+/// {
+/// "gitHash": "fe4a4029a080bc955e9588d05a6cd9eb490845d4",
+/// "key": {
+/// "arch": "x86",
+/// "gpu": "GTX660",
+/// "model": "ShuttleA",
+/// "os": "Ubuntu12"
+/// },
+/// "results": {
+/// "ChunkAlloc_PushPop_640_480": {
+/// "nonrendering": {
+/// "min_ms": 0.01485466666666667,
+/// "options": {
+/// "source_type": "bench"
+/// }
+/// }
+/// },
+/// "DeferredSurfaceCopy_discardable_640_480": {
+/// "565": {
+/// "min_ms": 2.215988,
+/// "options": {
+/// "source_type": "bench"
+/// }
+/// }
+/// }
+/// }
+/// }
+/// }
+/// ```
+class SkiaPerfPoint extends MetricPoint {
+ SkiaPerfPoint._(this.githubRepo, this.gitHash, this.testName, this.subResult,
+ double value, this._options, this.jsonUrl)
+ : assert(_options[kGithubRepoKey] == null),
+ assert(_options[kGitRevisionKey] == null),
+ assert(_options[kNameKey] == null),
+ super(
+ value,
+ <String, String>{}
+ ..addAll(_options)
+ ..addAll(<String, String>{
+ kGithubRepoKey: githubRepo,
+ kGitRevisionKey: gitHash,
+ kNameKey: testName,
+ kSubResultKey: subResult,
+ }),
+ ) {
+ assert(tags[kGithubRepoKey] != null);
+ assert(tags[kGitRevisionKey] != null);
+ assert(tags[kNameKey] != null);
+ }
+
+ /// Construct [SkiaPerfPoint] from a well-formed [MetricPoint].
+ ///
+ /// The [MetricPoint] must have [kGithubRepoKey], [kGitRevisionKey],
+ /// [kNameKey] in its tags for this to be successful.
+ ///
+ /// If the [MetricPoint] has a tag 'date', that tag will be removed so Skia
+ /// perf can plot multiple metrics with different date as a single trace.
+ /// Skia perf will use the git revision's date instead of this date tag in
+ /// the time axis.
+ factory SkiaPerfPoint.fromPoint(MetricPoint p) {
+ final String githubRepo = p.tags[kGithubRepoKey];
+ final String gitHash = p.tags[kGitRevisionKey];
+ final String name = p.tags[kNameKey];
+
+ if (githubRepo == null || gitHash == null || name == null) {
+ throw '$kGithubRepoKey, $kGitRevisionKey, $kNameKey must be set in'
+ ' the tags of $p.';
+ }
+
+ final String subResult = p.tags[kSubResultKey] ?? kSkiaPerfValueKey;
+
+ final Map<String, String> options = <String, String>{}..addEntries(
+ p.tags.entries.where(
+ (MapEntry<String, dynamic> entry) =>
+ entry.key != kGithubRepoKey &&
+ entry.key != kGitRevisionKey &&
+ entry.key != kNameKey &&
+ entry.key != kSubResultKey &&
+ // https://github.com/google/benchmark automatically generates a
+ // 'date' field. If it's included in options, the Skia perf won't
+ // be able to connect different points in a single trace because
+ // the date is always different.
+ entry.key != 'date',
+ ),
+ );
+
+ return SkiaPerfPoint._(
+ githubRepo, gitHash, name, subResult, p.value, options, null);
+ }
+
+ /// In the format of '<owner>/<name>' such as 'flutter/flutter' or
+ /// 'flutter/engine'.
+ final String githubRepo;
+
+ /// SHA such as 'ad20d368ffa09559754e4b2b5c12951341ca3b2d'
+ final String gitHash;
+
+ /// For Flutter devicelab, this is the task name (e.g.,
+ /// 'flutter_gallery__transition_perf'); for Google benchmark, this is the
+ /// benchmark name (e.g., 'BM_ShellShutdown').
+ ///
+ /// In Skia perf web dashboard, this value can be queried and filtered by
+ /// "test".
+ final String testName;
+
+ /// The name of "subResult" comes from the special treatment of "sub_result"
+ /// in SkiaPerf. If not provided, its value will be set to kSkiaPerfValueKey.
+ ///
+ /// When Google benchmarks are converted to SkiaPerfPoint, this subResult
+ /// could be "cpu_time" or "real_time".
+ ///
+ /// When devicelab benchmarks are converted to SkiaPerfPoint, this subResult
+ /// is often the metric name such as "average_frame_build_time_millis" whereas
+ /// the [testName] is the benchmark or task name such as
+ /// "flutter_gallery__transition_perf".
+ final String subResult;
+
+ /// The url to the Skia perf json file in the Google Cloud Storage bucket.
+ ///
+ /// This can be null if the point has been stored in the bucket yet.
+ final String jsonUrl;
+
+ Map<String, dynamic> _toSubResultJson() {
+ return <String, dynamic>{
+ subResult: value,
+ kSkiaPerfOptionsKey: _options,
+ };
+ }
+
+ /// Convert a list of SkiaPoints with the same git repo and git revision into
+ /// a single json file in the Skia perf format.
+ ///
+ /// The list must be non-empty.
+ static Map<String, dynamic> toSkiaPerfJson(List<SkiaPerfPoint> points) {
+ assert(points.isNotEmpty);
+ assert(() {
+ for (final SkiaPerfPoint p in points) {
+ if (p.githubRepo != points[0].githubRepo ||
+ p.gitHash != points[0].gitHash) {
+ return false;
+ }
+ }
+ return true;
+ }(), 'All points must have same githubRepo and gitHash');
+
+ final Map<String, dynamic> results = <String, dynamic>{};
+ for (final SkiaPerfPoint p in points) {
+ final Map<String, dynamic> subResultJson = p._toSubResultJson();
+ if (results[p.testName] == null) {
+ results[p.testName] = <String, dynamic>{
+ kSkiaPerfDefaultConfig: subResultJson,
+ };
+ } else {
+ // Flutter currently doesn't support having the same name but different
+ // options/configurations. If this actually happens in the future, we
+ // probably can use different values of config (currently there's only
+ // one kSkiaPerfDefaultConfig) to resolve the conflict.
+ assert(results[p.testName][kSkiaPerfDefaultConfig][kSkiaPerfOptionsKey]
+ .toString() ==
+ subResultJson[kSkiaPerfOptionsKey].toString());
+ assert(
+ results[p.testName][kSkiaPerfDefaultConfig][p.subResult] == null);
+ results[p.testName][kSkiaPerfDefaultConfig][p.subResult] = p.value;
+ }
+ }
+
+ return <String, dynamic>{
+ kSkiaPerfGitHashKey: points[0].gitHash,
+ kSkiaPerfResultsKey: results,
+ };
+ }
+
+ // Equivalent to tags without git repo, git hash, and name because those two
+ // are already stored somewhere else.
+ final Map<String, String> _options;
+}
+
+/// Handle writing and updates of Skia perf GCS buckets.
+class SkiaPerfGcsAdaptor {
+ /// Construct the adaptor given the associated GCS bucket where the data is
+ /// read from and written to.
+ SkiaPerfGcsAdaptor(this._gcsBucket) : assert(_gcsBucket != null);
+
+ /// Used by Skia to differentiate json file format versions.
+ static const int version = 1;
+
+ /// Write a list of SkiaPerfPoint into a GCS file with name `objectName` in
+ /// the proper json format that's understandable by Skia perf services.
+ ///
+ /// The `objectName` must be a properly formatted string returned by
+ /// [computeObjectName].
+ ///
+ /// The read may retry multiple times if transient network errors with code
+ /// 504 happens.
+ Future<void> writePoints(
+ String objectName, List<SkiaPerfPoint> points) async {
+ final String jsonString = jsonEncode(SkiaPerfPoint.toSkiaPerfJson(points));
+ final List<int> content = utf8.encode(jsonString);
+
+ // Retry multiple times as GCS may return 504 timeout.
+ for (int retry = 0; retry < 5; retry += 1) {
+ try {
+ await _gcsBucket.writeBytes(objectName, content);
+ return;
+ } catch (e) {
+ if (e is DetailedApiRequestError && e.status == 504) {
+ continue;
+ }
+ rethrow;
+ }
+ }
+ // Retry one last time and let the exception go through.
+ await _gcsBucket.writeBytes(objectName, content);
+ }
+
+ /// Read a list of `SkiaPerfPoint` that have been previously written to the
+ /// GCS file with name `objectName`.
+ ///
+ /// The Github repo and revision of those points will be inferred from the
+ /// `objectName`.
+ ///
+ /// Return an empty list if the object does not exist in the GCS bucket.
+ ///
+ /// The read may retry multiple times if transient network errors with code
+ /// 504 happens.
+ Future<List<SkiaPerfPoint>> readPoints(String objectName) async {
+ // Retry multiple times as GCS may return 504 timeout.
+ for (int retry = 0; retry < 5; retry += 1) {
+ try {
+ return await _readPointsWithoutRetry(objectName);
+ } catch (e) {
+ if (e is DetailedApiRequestError && e.status == 504) {
+ continue;
+ }
+ rethrow;
+ }
+ }
+ // Retry one last time and let the exception go through.
+ return _readPointsWithoutRetry(objectName);
+ }
+
+ Future<List<SkiaPerfPoint>> _readPointsWithoutRetry(String objectName) async {
+ ObjectInfo info;
+
+ try {
+ info = await _gcsBucket.info(objectName);
+ } catch (e) {
+ if (e.toString().contains('No such object')) {
+ return <SkiaPerfPoint>[];
+ } else {
+ rethrow;
+ }
+ }
+
+ final Stream<List<int>> stream = _gcsBucket.read(objectName);
+ final Stream<int> byteStream = stream.expand((List<int> x) => x);
+ final Map<String, dynamic> decodedJson =
+ jsonDecode(utf8.decode(await byteStream.toList()))
+ as Map<String, dynamic>;
+
+ final List<SkiaPerfPoint> points = <SkiaPerfPoint>[];
+
+ final String firstGcsNameComponent = objectName.split('/')[0];
+ _populateGcsNameToGithubRepoMapIfNeeded();
+ final String githubRepo = _gcsNameToGithubRepo[firstGcsNameComponent];
+ assert(githubRepo != null);
+
+ final String gitHash = decodedJson[kSkiaPerfGitHashKey] as String;
+ final Map<String, dynamic> results =
+ decodedJson[kSkiaPerfResultsKey] as Map<String, dynamic>;
+ for (final String name in results.keys) {
+ final Map<String, dynamic> subResultMap =
+ results[name][kSkiaPerfDefaultConfig] as Map<String, dynamic>;
+ for (final String subResult
+ in subResultMap.keys.where((String s) => s != kSkiaPerfOptionsKey)) {
+ points.add(SkiaPerfPoint._(
+ githubRepo,
+ gitHash,
+ name,
+ subResult,
+ subResultMap[subResult] as double,
+ (subResultMap[kSkiaPerfOptionsKey] as Map<String, dynamic>)
+ .cast<String, String>(),
+ info.downloadLink.toString(),
+ ));
+ }
+ }
+ return points;
+ }
+
+ /// Compute the GCS file name that's used to store metrics for a given commit
+ /// (git revision).
+ ///
+ /// Skia perf needs all directory names to be well formatted. The final name
+ /// of the json file (currently `values.json`) can be arbitrary, and multiple
+ /// json files can be put in that leaf directory. We intend to use multiple
+ /// json files in the future to scale up the system if too many writes are
+ /// competing for the same json file.
+ static Future<String> computeObjectName(
+ String githubRepo, String revision, DateTime commitTime) async {
+ assert(_githubRepoToGcsName[githubRepo] != null);
+ final String topComponent = _githubRepoToGcsName[githubRepo];
+ // [commitTime] is not guranteed to be UTC. Ensure it is so all results
+ // pushed to GCS are the same timezone.
+ final DateTime commitUtcTime = commitTime.toUtc();
+ final String month = commitUtcTime.month.toString().padLeft(2, '0');
+ final String day = commitUtcTime.day.toString().padLeft(2, '0');
+ final String hour = commitUtcTime.hour.toString().padLeft(2, '0');
+ final String dateComponents = '${commitUtcTime.year}/$month/$day/$hour';
+ return '$topComponent/$dateComponents/$revision/values.json';
+ }
+
+ static final Map<String, String> _githubRepoToGcsName = <String, String>{
+ kFlutterFrameworkRepo: 'flutter-flutter',
+ kFlutterEngineRepo: 'flutter-engine',
+ };
+ static final Map<String, String> _gcsNameToGithubRepo = <String, String>{};
+
+ static void _populateGcsNameToGithubRepoMapIfNeeded() {
+ if (_gcsNameToGithubRepo.isEmpty) {
+ for (final String repo in _githubRepoToGcsName.keys) {
+ final String gcsName = _githubRepoToGcsName[repo];
+ assert(_gcsNameToGithubRepo[gcsName] == null);
+ _gcsNameToGithubRepo[gcsName] = repo;
+ }
+ }
+ }
+
+ final Bucket _gcsBucket;
+}
+
+/// A [MetricDestination] that conforms to Skia Perf's protocols.
+class SkiaPerfDestination extends MetricDestination {
+ /// Creates a new [SkiaPerfDestination].
+ SkiaPerfDestination(this._gcs, this._lock);
+
+ /// Create from a full credentials json (of a service account).
+ static Future<SkiaPerfDestination> makeFromGcpCredentials(
+ Map<String, dynamic> credentialsJson,
+ {bool isTesting = false}) async {
+ final AutoRefreshingAuthClient client = await clientViaServiceAccount(
+ ServiceAccountCredentials.fromJson(credentialsJson), Storage.SCOPES);
+ return make(
+ client,
+ credentialsJson[kProjectId] as String,
+ isTesting: isTesting,
+ );
+ }
+
+ /// Create from an access token and its project id.
+ static Future<SkiaPerfDestination> makeFromAccessToken(
+ String token, String projectId,
+ {bool isTesting = false}) async {
+ final AuthClient client = authClientFromAccessToken(token, Storage.SCOPES);
+ return make(client, projectId, isTesting: isTesting);
+ }
+
+ /// Create from an [AuthClient] and a GCP project id.
+ ///
+ /// [AuthClient] can be obtained from functions like `clientViaUserConsent`.
+ static Future<SkiaPerfDestination> make(AuthClient client, String projectId,
+ {bool isTesting = false}) async {
+ final Storage storage = Storage(client, projectId);
+ final String bucketName = isTesting ? kTestBucketName : kBucketName;
+ if (!await storage.bucketExists(bucketName)) {
+ throw 'Bucket $bucketName does not exist.';
+ }
+ final SkiaPerfGcsAdaptor adaptor =
+ SkiaPerfGcsAdaptor(storage.bucket(bucketName));
+ final GcsLock lock = GcsLock(client, bucketName);
+ return SkiaPerfDestination(adaptor, lock);
+ }
+
+ @override
+ Future<void> update(List<MetricPoint> points, DateTime commitTime) async {
+ // 1st, create a map based on git repo, git revision, and point id. Git repo
+ // and git revision are the top level components of the Skia perf GCS object
+ // name.
+ final Map<String, Map<String, Map<String, SkiaPerfPoint>>> pointMap =
+ <String, Map<String, Map<String, SkiaPerfPoint>>>{};
+ for (final SkiaPerfPoint p
+ in points.map((MetricPoint x) => SkiaPerfPoint.fromPoint(x))) {
+ if (p != null) {
+ pointMap[p.githubRepo] ??= <String, Map<String, SkiaPerfPoint>>{};
+ pointMap[p.githubRepo][p.gitHash] ??= <String, SkiaPerfPoint>{};
+ pointMap[p.githubRepo][p.gitHash][p.id] = p;
+ }
+ }
+
+ // 2nd, read existing points from the gcs object and update with new ones.
+ for (final String repo in pointMap.keys) {
+ for (final String revision in pointMap[repo].keys) {
+ final String objectName = await SkiaPerfGcsAdaptor.computeObjectName(
+ repo, revision, commitTime);
+ final Map<String, SkiaPerfPoint> newPoints = pointMap[repo][revision];
+ // If too many bots are writing the metrics of a git revision into this
+ // single json file (with name `objectName`), the contention on the lock
+ // might be too high. In that case, break the json file into multiple
+ // json files according to bot names or task names. Skia perf read all
+ // json files in the directory so one can use arbitrary names for those
+ // sharded json file names.
+ _lock.protectedRun('$objectName.lock', () async {
+ final List<SkiaPerfPoint> oldPoints =
+ await _gcs.readPoints(objectName);
+ for (final SkiaPerfPoint p in oldPoints) {
+ if (newPoints[p.id] == null) {
+ newPoints[p.id] = p;
+ }
+ }
+ await _gcs.writePoints(objectName, newPoints.values.toList());
+ });
+ }
+ }
+ }
+
+ final SkiaPerfGcsAdaptor _gcs;
+ final GcsLock _lock;
+}
diff --git a/packages/metrics_center/pubspec.yaml b/packages/metrics_center/pubspec.yaml
new file mode 100644
index 0000000..cc8fac0
--- /dev/null
+++ b/packages/metrics_center/pubspec.yaml
@@ -0,0 +1,23 @@
+name: metrics_center
+version: 0.1.0
+description:
+ Support multiple performance metrics sources/formats and destinations.
+homepage:
+ https://github.com/flutter/packages/tree/master/packages/metrics_center
+
+environment:
+ sdk: '>=2.10.0 <3.0.0'
+
+dependencies:
+ crypto: ^2.1.5
+ equatable: ^1.2.5
+ gcloud: ^0.7.3
+ googleapis: ^0.56.1
+ googleapis_auth: ^0.2.12
+ http: ^0.12.2
+
+dev_dependencies:
+ fake_async: ^1.2.0-nullsafety.3
+ mockito: ^4.1.1
+ pedantic: ^1.10.0-nullsafety.3
+ test: ^1.16.0-nullsafety.9
diff --git a/packages/metrics_center/test/common.dart b/packages/metrics_center/test/common.dart
new file mode 100644
index 0000000..6ca543f
--- /dev/null
+++ b/packages/metrics_center/test/common.dart
@@ -0,0 +1,27 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
+import 'package:test/test.dart' as test_package show TypeMatcher;
+
+export 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
+
+// Defines a 'package:test' shim.
+// TODO(ianh): Remove this file once https://github.com/dart-lang/matcher/issues/98 is fixed
+
+/// A matcher that compares the type of the actual value to the type argument T.
+test_package.TypeMatcher<T> isInstanceOf<T>() => isA<T>();
+
+void tryToDelete(Directory directory) {
+ // This should not be necessary, but it turns out that
+ // on Windows it's common for deletions to fail due to
+ // bogus (we think) "access denied" errors.
+ try {
+ directory.deleteSync(recursive: true);
+ } on FileSystemException catch (error) {
+ print('Failed to delete ${directory.path}: $error');
+ }
+}
diff --git a/packages/metrics_center/test/example_google_benchmark.json b/packages/metrics_center/test/example_google_benchmark.json
new file mode 100644
index 0000000..212ce9c
--- /dev/null
+++ b/packages/metrics_center/test/example_google_benchmark.json
@@ -0,0 +1,32 @@
+{
+ "context": {
+ "date": "2019-12-17 15:14:14",
+ "num_cpus": 56,
+ "mhz_per_cpu": 2594,
+ "cpu_scaling_enabled": true,
+ "library_build_type": "release"
+ },
+ "benchmarks": [
+ {
+ "name": "BM_PaintRecordInit",
+ "iterations": 6749079,
+ "real_time": 101,
+ "cpu_time": 101,
+ "time_unit": "ns"
+ },
+ {
+ "name": "BM_ParagraphShortLayout",
+ "iterations": 151761,
+ "real_time": 4460,
+ "cpu_time": 4460,
+ "time_unit": "ns"
+ },
+ {
+ "name": "BM_ParagraphStylesBigO_BigO",
+ "cpu_coefficient": 6548,
+ "real_coefficient": 6548,
+ "big_o": "N",
+ "time_unit": "ns"
+ }
+ ]
+}
diff --git a/packages/metrics_center/test/flutter_test.dart b/packages/metrics_center/test/flutter_test.dart
new file mode 100644
index 0000000..277e400
--- /dev/null
+++ b/packages/metrics_center/test/flutter_test.dart
@@ -0,0 +1,50 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:metrics_center/src/constants.dart';
+import 'package:metrics_center/src/flutter.dart';
+
+import 'common.dart';
+import 'utility.dart';
+
+void main() {
+ const String gitRevision = 'ca799fa8b2254d09664b78ee80c43b434788d112';
+ final FlutterEngineMetricPoint simplePoint = FlutterEngineMetricPoint(
+ 'BM_ParagraphLongLayout',
+ 287235,
+ gitRevision,
+ );
+
+ test('FlutterEngineMetricPoint works.', () {
+ expect(simplePoint.value, equals(287235));
+ expect(simplePoint.tags[kGithubRepoKey], kFlutterEngineRepo);
+ expect(simplePoint.tags[kGitRevisionKey], gitRevision);
+ expect(simplePoint.tags[kNameKey], 'BM_ParagraphLongLayout');
+
+ final FlutterEngineMetricPoint detailedPoint = FlutterEngineMetricPoint(
+ 'BM_ParagraphLongLayout',
+ 287224,
+ 'ca799fa8b2254d09664b78ee80c43b434788d112',
+ moreTags: const <String, String>{
+ 'executable': 'txt_benchmarks',
+ 'sub_result': 'CPU',
+ kUnitKey: 'ns',
+ },
+ );
+ expect(detailedPoint.value, equals(287224));
+ expect(detailedPoint.tags['executable'], equals('txt_benchmarks'));
+ expect(detailedPoint.tags['sub_result'], equals('CPU'));
+ expect(detailedPoint.tags[kUnitKey], equals('ns'));
+ });
+
+ final Map<String, dynamic> credentialsJson = getTestGcpCredentialsJson();
+
+ test('FlutterDestination integration test with update.', () async {
+ final FlutterDestination dst =
+ await FlutterDestination.makeFromCredentialsJson(credentialsJson,
+ isTesting: true);
+ dst.update(<FlutterEngineMetricPoint>[simplePoint],
+ DateTime.fromMillisecondsSinceEpoch(123));
+ }, skip: credentialsJson == null);
+}
diff --git a/packages/metrics_center/test/gcs_lock_test.dart b/packages/metrics_center/test/gcs_lock_test.dart
new file mode 100644
index 0000000..d682d95
--- /dev/null
+++ b/packages/metrics_center/test/gcs_lock_test.dart
@@ -0,0 +1,105 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:googleapis/storage/v1.dart';
+import 'package:fake_async/fake_async.dart';
+import 'package:gcloud/storage.dart';
+import 'package:googleapis_auth/auth_io.dart';
+import 'package:metrics_center/src/constants.dart';
+import 'package:metrics_center/src/gcs_lock.dart';
+import 'package:mockito/mockito.dart';
+
+import 'common.dart';
+import 'utility.dart';
+
+enum TestPhase {
+ run1,
+ run2,
+}
+
+class MockClient extends Mock implements AuthClient {}
+
+void main() {
+ const Duration kDelayStep = Duration(milliseconds: 10);
+ final Map<String, dynamic> credentialsJson = getTestGcpCredentialsJson();
+
+ test('GcsLock prints warnings for long waits', () {
+ // Capture print to verify error messages.
+ final List<String> prints = <String>[];
+ final ZoneSpecification spec =
+ ZoneSpecification(print: (_, __, ___, String msg) => prints.add(msg));
+
+ Zone.current.fork(specification: spec).run<void>(() {
+ fakeAsync((FakeAsync fakeAsync) {
+ final MockClient mockClient = MockClient();
+ final GcsLock lock = GcsLock(mockClient, 'mockBucket');
+ when(mockClient.send(any)).thenThrow(DetailedApiRequestError(412, ''));
+ final Future<void> runFinished =
+ lock.protectedRun('mock.lock', () async {});
+ fakeAsync.elapse(const Duration(seconds: 10));
+ when(mockClient.send(any)).thenThrow(AssertionError('Stop!'));
+ runFinished.catchError((dynamic e) {
+ final AssertionError error = e as AssertionError;
+ expect(error.message, 'Stop!');
+ print('${error.message}');
+ });
+ fakeAsync.elapse(const Duration(seconds: 20));
+ });
+ });
+
+ const String kExpectedErrorMessage = 'The lock is waiting for a long time: '
+ '0:00:10.240000. If the lock file mock.lock in bucket mockBucket '
+ 'seems to be stuck (i.e., it was created a long time ago and no one '
+ 'seems to be owning it currently), delete it manually to unblock this.';
+ expect(prints, equals(<String>[kExpectedErrorMessage, 'Stop!']));
+ });
+
+ test('GcsLock integration test: single protectedRun is successful', () async {
+ final AutoRefreshingAuthClient client = await clientViaServiceAccount(
+ ServiceAccountCredentials.fromJson(credentialsJson), Storage.SCOPES);
+ final GcsLock lock = GcsLock(client, kTestBucketName);
+ int testValue = 0;
+ await lock.protectedRun('test.lock', () async {
+ testValue = 1;
+ });
+ expect(testValue, 1);
+ }, skip: credentialsJson == null);
+
+ test('GcsLock integration test: protectedRun is exclusive', () async {
+ final AutoRefreshingAuthClient client = await clientViaServiceAccount(
+ ServiceAccountCredentials.fromJson(credentialsJson), Storage.SCOPES);
+ final GcsLock lock1 = GcsLock(client, kTestBucketName);
+ final GcsLock lock2 = GcsLock(client, kTestBucketName);
+
+ TestPhase phase = TestPhase.run1;
+ final Completer<void> started1 = Completer<void>();
+ final Future<void> finished1 = lock1.protectedRun('test.lock', () async {
+ started1.complete();
+ while (phase == TestPhase.run1) {
+ await Future<void>.delayed(kDelayStep);
+ }
+ });
+
+ await started1.future;
+
+ final Completer<void> started2 = Completer<void>();
+ final Future<void> finished2 = lock2.protectedRun('test.lock', () async {
+ started2.complete();
+ });
+
+ // started2 should not be set even after a long wait because lock1 is
+ // holding the GCS lock file.
+ await Future<void>.delayed(kDelayStep * 10);
+ expect(started2.isCompleted, false);
+
+ // When phase is switched to run2, lock1 should be released soon and
+ // lock2 should soon be able to proceed its protectedRun.
+ phase = TestPhase.run2;
+ await started2.future;
+ await finished1;
+ await finished2;
+ }, skip: credentialsJson == null);
+}
diff --git a/packages/metrics_center/test/google_benchmark_test.dart b/packages/metrics_center/test/google_benchmark_test.dart
new file mode 100644
index 0000000..11883ae
--- /dev/null
+++ b/packages/metrics_center/test/google_benchmark_test.dart
@@ -0,0 +1,39 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:metrics_center/src/constants.dart';
+import 'package:metrics_center/src/common.dart';
+import 'package:metrics_center/src/google_benchmark.dart';
+
+import 'common.dart';
+import 'utility.dart';
+
+void main() {
+ test('GoogleBenchmarkParser parses example json.', () async {
+ final List<MetricPoint> points =
+ await GoogleBenchmarkParser.parse('test/example_google_benchmark.json');
+ expect(points.length, 6);
+ expectSetMatch(
+ points.map((MetricPoint p) => p.value),
+ <int>[101, 101, 4460, 4460, 6548, 6548],
+ );
+ expectSetMatch(
+ points.map((MetricPoint p) => p.tags[kSubResultKey]),
+ <String>[
+ 'cpu_time',
+ 'real_time',
+ 'cpu_coefficient',
+ 'real_coefficient',
+ ],
+ );
+ expectSetMatch(
+ points.map((MetricPoint p) => p.tags[kNameKey]),
+ <String>[
+ 'BM_PaintRecordInit',
+ 'BM_ParagraphShortLayout',
+ 'BM_ParagraphStylesBigO_BigO',
+ ],
+ );
+ });
+}
diff --git a/packages/metrics_center/test/skiaperf_test.dart b/packages/metrics_center/test/skiaperf_test.dart
new file mode 100644
index 0000000..d815711
--- /dev/null
+++ b/packages/metrics_center/test/skiaperf_test.dart
@@ -0,0 +1,583 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@Timeout(Duration(seconds: 3600))
+
+import 'dart:convert';
+
+import 'package:gcloud/storage.dart';
+import 'package:googleapis/storage/v1.dart' show DetailedApiRequestError;
+import 'package:googleapis_auth/auth_io.dart';
+import 'package:metrics_center/metrics_center.dart';
+import 'package:mockito/mockito.dart';
+
+import 'package:metrics_center/src/constants.dart';
+import 'package:metrics_center/src/gcs_lock.dart';
+
+import 'common.dart';
+import 'utility.dart';
+
+class MockBucket extends Mock implements Bucket {}
+
+class MockObjectInfo extends Mock implements ObjectInfo {}
+
+class MockGcsLock implements GcsLock {
+ @override
+ Future<void> protectedRun(
+ String exclusiveObjectName, Future<void> Function() f) async {
+ await f();
+ }
+}
+
+class MockSkiaPerfGcsAdaptor implements SkiaPerfGcsAdaptor {
+ @override
+ Future<List<SkiaPerfPoint>> readPoints(String objectName) async {
+ return _storage[objectName] ?? <SkiaPerfPoint>[];
+ }
+
+ @override
+ Future<void> writePoints(
+ String objectName, List<SkiaPerfPoint> points) async {
+ _storage[objectName] = points.toList();
+ }
+
+ // Map from the object name to the list of SkiaPoint that mocks the GCS.
+ final Map<String, List<SkiaPerfPoint>> _storage =
+ <String, List<SkiaPerfPoint>>{};
+}
+
+Future<void> main() async {
+ const double kValue1 = 1.0;
+ const double kValue2 = 2.0;
+ const double kValue3 = 3.0;
+
+ const String kFrameworkRevision1 = '9011cece2595447eea5dd91adaa241c1c9ef9a33';
+ const String kFrameworkRevision2 = '372fe290e4d4f3f97cbf02a57d235771a9412f10';
+ const String kEngineRevision1 = '617938024315e205f26ed72ff0f0647775fa6a71';
+ const String kEngineRevision2 = '5858519139c22484aaff1cf5b26bdf7951259344';
+ const String kTaskName = 'analyzer_benchmark';
+ const String kMetric1 = 'flutter_repo_batch_maximum';
+ const String kMetric2 = 'flutter_repo_watch_maximum';
+
+ final MetricPoint cocoonPointRev1Metric1 = MetricPoint(
+ kValue1,
+ const <String, String>{
+ kGithubRepoKey: kFlutterFrameworkRepo,
+ kGitRevisionKey: kFrameworkRevision1,
+ kNameKey: kTaskName,
+ kSubResultKey: kMetric1,
+ kUnitKey: 's',
+ },
+ );
+
+ final MetricPoint cocoonPointRev1Metric2 = MetricPoint(
+ kValue2,
+ const <String, String>{
+ kGithubRepoKey: kFlutterFrameworkRepo,
+ kGitRevisionKey: kFrameworkRevision1,
+ kNameKey: kTaskName,
+ kSubResultKey: kMetric2,
+ kUnitKey: 's',
+ },
+ );
+
+ final MetricPoint cocoonPointRev2Metric1 = MetricPoint(
+ kValue3,
+ const <String, String>{
+ kGithubRepoKey: kFlutterFrameworkRepo,
+ kGitRevisionKey: kFrameworkRevision2,
+ kNameKey: kTaskName,
+ kSubResultKey: kMetric1,
+ kUnitKey: 's',
+ },
+ );
+
+ final MetricPoint cocoonPointBetaRev1Metric1 = MetricPoint(
+ kValue1,
+ const <String, String>{
+ kGithubRepoKey: kFlutterFrameworkRepo,
+ kGitRevisionKey: kFrameworkRevision1,
+ kNameKey: 'beta/$kTaskName',
+ kSubResultKey: kMetric1,
+ kUnitKey: 's',
+ 'branch': 'beta',
+ },
+ );
+
+ final MetricPoint cocoonPointBetaRev1Metric1BadBranch = MetricPoint(
+ kValue1,
+ const <String, String>{
+ kGithubRepoKey: kFlutterFrameworkRepo,
+ kGitRevisionKey: kFrameworkRevision1,
+ kNameKey: kTaskName,
+ kSubResultKey: kMetric1,
+ kUnitKey: 's',
+
+ // If we only add this 'branch' tag without changing the test or sub-result name, an exception
+ // would be thrown as Skia Perf currently only supports the same set of tags for a pair of
+ // kNameKey and kSubResultKey values. So to support branches, one also has to add the branch
+ // name to the test name.
+ 'branch': 'beta',
+ },
+ );
+
+ const String engineMetricName = 'BM_PaintRecordInit';
+ const String engineRevision = 'ca799fa8b2254d09664b78ee80c43b434788d112';
+ const double engineValue1 = 101;
+ const double engineValue2 = 102;
+
+ final FlutterEngineMetricPoint enginePoint1 = FlutterEngineMetricPoint(
+ engineMetricName,
+ engineValue1,
+ engineRevision,
+ moreTags: const <String, String>{
+ kSubResultKey: 'cpu_time',
+ kUnitKey: 'ns',
+ 'date': '2019-12-17 15:14:14',
+ 'num_cpus': '56',
+ 'mhz_per_cpu': '2594',
+ 'cpu_scaling_enabled': 'true',
+ 'library_build_type': 'release',
+ },
+ );
+
+ final FlutterEngineMetricPoint enginePoint2 = FlutterEngineMetricPoint(
+ engineMetricName,
+ engineValue2,
+ engineRevision,
+ moreTags: const <String, String>{
+ kSubResultKey: 'real_time',
+ kUnitKey: 'ns',
+ 'date': '2019-12-17 15:14:14',
+ 'num_cpus': '56',
+ 'mhz_per_cpu': '2594',
+ 'cpu_scaling_enabled': 'true',
+ 'library_build_type': 'release',
+ },
+ );
+
+ test('Throw if invalid points are converted to SkiaPoint', () {
+ final MetricPoint noGithubRepoPoint = MetricPoint(
+ kValue1,
+ const <String, String>{
+ kGitRevisionKey: kFrameworkRevision1,
+ kNameKey: kTaskName,
+ },
+ );
+
+ final MetricPoint noGitRevisionPoint = MetricPoint(
+ kValue1,
+ const <String, String>{
+ kGithubRepoKey: kFlutterFrameworkRepo,
+ kNameKey: kTaskName,
+ },
+ );
+
+ final MetricPoint noTestNamePoint = MetricPoint(
+ kValue1,
+ const <String, String>{
+ kGithubRepoKey: kFlutterFrameworkRepo,
+ kGitRevisionKey: kFrameworkRevision1,
+ },
+ );
+
+ expect(() => SkiaPerfPoint.fromPoint(noGithubRepoPoint), throwsA(anything));
+ expect(
+ () => SkiaPerfPoint.fromPoint(noGitRevisionPoint), throwsA(anything));
+ expect(() => SkiaPerfPoint.fromPoint(noTestNamePoint), throwsA(anything));
+ });
+
+ test('Correctly convert a metric point from cocoon to SkiaPoint', () {
+ final SkiaPerfPoint skiaPoint1 =
+ SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1);
+ expect(skiaPoint1, isNotNull);
+ expect(skiaPoint1.testName, equals(kTaskName));
+ expect(skiaPoint1.subResult, equals(kMetric1));
+ expect(skiaPoint1.value, equals(cocoonPointRev1Metric1.value));
+ expect(skiaPoint1.jsonUrl, isNull); // Not inserted yet
+ });
+
+ test('Cocoon points correctly encode into Skia perf json format', () {
+ final SkiaPerfPoint p1 = SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1);
+ final SkiaPerfPoint p2 = SkiaPerfPoint.fromPoint(cocoonPointRev1Metric2);
+ final SkiaPerfPoint p3 =
+ SkiaPerfPoint.fromPoint(cocoonPointBetaRev1Metric1);
+
+ const JsonEncoder encoder = JsonEncoder.withIndent(' ');
+
+ expect(
+ encoder
+ .convert(SkiaPerfPoint.toSkiaPerfJson(<SkiaPerfPoint>[p1, p2, p3])),
+ equals('''
+{
+ "gitHash": "9011cece2595447eea5dd91adaa241c1c9ef9a33",
+ "results": {
+ "analyzer_benchmark": {
+ "default": {
+ "flutter_repo_batch_maximum": 1.0,
+ "options": {
+ "unit": "s"
+ },
+ "flutter_repo_watch_maximum": 2.0
+ }
+ },
+ "beta/analyzer_benchmark": {
+ "default": {
+ "flutter_repo_batch_maximum": 1.0,
+ "options": {
+ "branch": "beta",
+ "unit": "s"
+ }
+ }
+ }
+ }
+}'''));
+ });
+
+ test('Engine metric points correctly encode into Skia perf json format', () {
+ const JsonEncoder encoder = JsonEncoder.withIndent(' ');
+ expect(
+ encoder.convert(SkiaPerfPoint.toSkiaPerfJson(<SkiaPerfPoint>[
+ SkiaPerfPoint.fromPoint(enginePoint1),
+ SkiaPerfPoint.fromPoint(enginePoint2),
+ ])),
+ equals(
+ '''
+{
+ "gitHash": "ca799fa8b2254d09664b78ee80c43b434788d112",
+ "results": {
+ "BM_PaintRecordInit": {
+ "default": {
+ "cpu_time": 101.0,
+ "options": {
+ "cpu_scaling_enabled": "true",
+ "library_build_type": "release",
+ "mhz_per_cpu": "2594",
+ "num_cpus": "56",
+ "unit": "ns"
+ },
+ "real_time": 102.0
+ }
+ }
+ }
+}''',
+ ),
+ );
+ });
+
+ test(
+ 'Throw if engine points with the same test name but different options are converted to '
+ 'Skia perf points', () {
+ final FlutterEngineMetricPoint enginePoint1 = FlutterEngineMetricPoint(
+ 'BM_PaintRecordInit',
+ 101,
+ 'ca799fa8b2254d09664b78ee80c43b434788d112',
+ moreTags: const <String, String>{
+ kSubResultKey: 'cpu_time',
+ kUnitKey: 'ns',
+ 'cpu_scaling_enabled': 'true',
+ },
+ );
+ final FlutterEngineMetricPoint enginePoint2 = FlutterEngineMetricPoint(
+ 'BM_PaintRecordInit',
+ 102,
+ 'ca799fa8b2254d09664b78ee80c43b434788d112',
+ moreTags: const <String, String>{
+ kSubResultKey: 'real_time',
+ kUnitKey: 'ns',
+ 'cpu_scaling_enabled': 'false',
+ },
+ );
+
+ const JsonEncoder encoder = JsonEncoder.withIndent(' ');
+ expect(
+ () => encoder.convert(SkiaPerfPoint.toSkiaPerfJson(<SkiaPerfPoint>[
+ SkiaPerfPoint.fromPoint(enginePoint1),
+ SkiaPerfPoint.fromPoint(enginePoint2),
+ ])),
+ throwsA(anything),
+ );
+ });
+
+ test(
+ 'Throw if two Cocoon metric points with the same name and subResult keys '
+ 'but different options are converted to Skia perf points', () {
+ final SkiaPerfPoint p1 = SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1);
+ final SkiaPerfPoint p2 =
+ SkiaPerfPoint.fromPoint(cocoonPointBetaRev1Metric1BadBranch);
+
+ expect(
+ () => SkiaPerfPoint.toSkiaPerfJson(<SkiaPerfPoint>[p1, p2]),
+ throwsA(anything),
+ );
+ });
+
+ test('SkiaPerfGcsAdaptor computes name correctly', () async {
+ expect(
+ await SkiaPerfGcsAdaptor.computeObjectName(
+ kFlutterFrameworkRepo,
+ kFrameworkRevision1,
+ DateTime.utc(2019, 12, 04, 23),
+ ),
+ equals('flutter-flutter/2019/12/04/23/$kFrameworkRevision1/values.json'),
+ );
+ expect(
+ await SkiaPerfGcsAdaptor.computeObjectName(
+ kFlutterEngineRepo,
+ kEngineRevision1,
+ DateTime.utc(2019, 12, 03, 20),
+ ),
+ equals('flutter-engine/2019/12/03/20/$kEngineRevision1/values.json'),
+ );
+ expect(
+ await SkiaPerfGcsAdaptor.computeObjectName(
+ kFlutterEngineRepo,
+ kEngineRevision2,
+ DateTime.utc(2020, 01, 03, 15),
+ ),
+ equals('flutter-engine/2020/01/03/15/$kEngineRevision2/values.json'),
+ );
+ });
+
+ test('Successfully read mock GCS that fails 1st time with 504', () async {
+ final MockBucket testBucket = MockBucket();
+ final SkiaPerfGcsAdaptor skiaPerfGcs = SkiaPerfGcsAdaptor(testBucket);
+
+ final String testObjectName = await SkiaPerfGcsAdaptor.computeObjectName(
+ kFlutterFrameworkRepo,
+ kFrameworkRevision1,
+ DateTime.fromMillisecondsSinceEpoch(123));
+
+ final List<SkiaPerfPoint> writePoints = <SkiaPerfPoint>[
+ SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1),
+ ];
+ final String skiaPerfJson =
+ jsonEncode(SkiaPerfPoint.toSkiaPerfJson(writePoints));
+ await skiaPerfGcs.writePoints(testObjectName, writePoints);
+ verify(testBucket.writeBytes(testObjectName, utf8.encode(skiaPerfJson)));
+
+ // Emulate the first network request to fail with 504.
+ when(testBucket.info(testObjectName))
+ .thenThrow(DetailedApiRequestError(504, 'Test Failure'));
+
+ final MockObjectInfo mockObjectInfo = MockObjectInfo();
+ when(mockObjectInfo.downloadLink)
+ .thenReturn(Uri.https('test.com', 'mock.json'));
+ when(testBucket.info(testObjectName))
+ .thenAnswer((_) => Future<ObjectInfo>.value(mockObjectInfo));
+ when(testBucket.read(testObjectName))
+ .thenAnswer((_) => Stream<List<int>>.value(utf8.encode(skiaPerfJson)));
+
+ final List<SkiaPerfPoint> readPoints =
+ await skiaPerfGcs.readPoints(testObjectName);
+ expect(readPoints.length, equals(1));
+ expect(readPoints[0].testName, kTaskName);
+ expect(readPoints[0].subResult, kMetric1);
+ expect(readPoints[0].value, kValue1);
+ expect(readPoints[0].githubRepo, kFlutterFrameworkRepo);
+ expect(readPoints[0].gitHash, kFrameworkRevision1);
+ expect(readPoints[0].jsonUrl, 'https://test.com/mock.json');
+ });
+
+ test('Return empty list if the GCS file does not exist', () async {
+ final MockBucket testBucket = MockBucket();
+ final SkiaPerfGcsAdaptor skiaPerfGcs = SkiaPerfGcsAdaptor(testBucket);
+ final String testObjectName = await SkiaPerfGcsAdaptor.computeObjectName(
+ kFlutterFrameworkRepo,
+ kFrameworkRevision1,
+ DateTime.fromMillisecondsSinceEpoch(123));
+ when(testBucket.info(testObjectName))
+ .thenThrow(Exception('No such object'));
+ expect((await skiaPerfGcs.readPoints(testObjectName)).length, 0);
+ });
+
+ // The following is for integration tests.
+ Bucket testBucket;
+ GcsLock testLock;
+ final Map<String, dynamic> credentialsJson = getTestGcpCredentialsJson();
+ if (credentialsJson != null) {
+ final ServiceAccountCredentials credentials =
+ ServiceAccountCredentials.fromJson(credentialsJson);
+
+ final AutoRefreshingAuthClient client =
+ await clientViaServiceAccount(credentials, Storage.SCOPES);
+ final Storage storage =
+ Storage(client, credentialsJson['project_id'] as String);
+
+ const String kTestBucketName = 'flutter-skia-perf-test';
+
+ assert(await storage.bucketExists(kTestBucketName));
+ testBucket = storage.bucket(kTestBucketName);
+ testLock = GcsLock(client, kTestBucketName);
+ }
+
+ Future<void> skiaPerfGcsAdapterIntegrationTest() async {
+ final SkiaPerfGcsAdaptor skiaPerfGcs = SkiaPerfGcsAdaptor(testBucket);
+
+ final String testObjectName = await SkiaPerfGcsAdaptor.computeObjectName(
+ kFlutterFrameworkRepo,
+ kFrameworkRevision1,
+ DateTime.fromMillisecondsSinceEpoch(123));
+
+ await skiaPerfGcs.writePoints(testObjectName, <SkiaPerfPoint>[
+ SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1),
+ SkiaPerfPoint.fromPoint(cocoonPointRev1Metric2),
+ ]);
+
+ final List<SkiaPerfPoint> points =
+ await skiaPerfGcs.readPoints(testObjectName);
+ expect(points.length, equals(2));
+ expectSetMatch(
+ points.map((SkiaPerfPoint p) => p.testName), <String>[kTaskName]);
+ expectSetMatch(points.map((SkiaPerfPoint p) => p.subResult),
+ <String>[kMetric1, kMetric2]);
+ expectSetMatch(
+ points.map((SkiaPerfPoint p) => p.value), <double>[kValue1, kValue2]);
+ expectSetMatch(points.map((SkiaPerfPoint p) => p.githubRepo),
+ <String>[kFlutterFrameworkRepo]);
+ expectSetMatch(points.map((SkiaPerfPoint p) => p.gitHash),
+ <String>[kFrameworkRevision1]);
+ for (int i = 0; i < 2; i += 1) {
+ expect(points[0].jsonUrl, startsWith('https://'));
+ }
+ }
+
+ Future<void> skiaPerfGcsIntegrationTestWithEnginePoints() async {
+ final SkiaPerfGcsAdaptor skiaPerfGcs = SkiaPerfGcsAdaptor(testBucket);
+
+ final String testObjectName = await SkiaPerfGcsAdaptor.computeObjectName(
+ kFlutterEngineRepo,
+ engineRevision,
+ DateTime.fromMillisecondsSinceEpoch(123));
+
+ await skiaPerfGcs.writePoints(testObjectName, <SkiaPerfPoint>[
+ SkiaPerfPoint.fromPoint(enginePoint1),
+ SkiaPerfPoint.fromPoint(enginePoint2),
+ ]);
+
+ final List<SkiaPerfPoint> points =
+ await skiaPerfGcs.readPoints(testObjectName);
+ expect(points.length, equals(2));
+ expectSetMatch(
+ points.map((SkiaPerfPoint p) => p.testName),
+ <String>[engineMetricName, engineMetricName],
+ );
+ expectSetMatch(
+ points.map((SkiaPerfPoint p) => p.value),
+ <double>[engineValue1, engineValue2],
+ );
+ expectSetMatch(
+ points.map((SkiaPerfPoint p) => p.githubRepo),
+ <String>[kFlutterEngineRepo],
+ );
+ expectSetMatch(
+ points.map((SkiaPerfPoint p) => p.gitHash), <String>[engineRevision]);
+ for (int i = 0; i < 2; i += 1) {
+ expect(points[0].jsonUrl, startsWith('https://'));
+ }
+ }
+
+ // To run the following integration tests, there must be a valid Google Cloud
+ // Project service account credentials in secret/test_gcp_credentials.json so
+ // `testBucket` won't be null. Currently, these integration tests are skipped
+ // in the CI, and only verified locally.
+ test(
+ 'SkiaPerfGcsAdaptor passes integration test with Google Cloud Storage',
+ skiaPerfGcsAdapterIntegrationTest,
+ skip: testBucket == null,
+ );
+
+ test(
+ 'SkiaPerfGcsAdaptor integration test with engine points',
+ skiaPerfGcsIntegrationTestWithEnginePoints,
+ skip: testBucket == null,
+ );
+
+ // `SkiaPerfGcsAdaptor.computeObjectName` uses `GithubHelper` which requires
+ // network connections. Hence we put them as integration tests instead of unit
+ // tests.
+ test(
+ 'SkiaPerfGcsAdaptor integration test for name computations',
+ () async {
+ expect(
+ await SkiaPerfGcsAdaptor.computeObjectName(
+ kFlutterFrameworkRepo,
+ kFrameworkRevision1,
+ DateTime.utc(2019, 12, 04, 23),
+ ),
+ equals(
+ 'flutter-flutter/2019/12/04/23/$kFrameworkRevision1/values.json'),
+ );
+ expect(
+ await SkiaPerfGcsAdaptor.computeObjectName(
+ kFlutterEngineRepo,
+ kEngineRevision1,
+ DateTime.utc(2019, 12, 03, 20),
+ ),
+ equals('flutter-engine/2019/12/03/20/$kEngineRevision1/values.json'),
+ );
+ expect(
+ await SkiaPerfGcsAdaptor.computeObjectName(
+ kFlutterEngineRepo,
+ kEngineRevision2,
+ DateTime.utc(2020, 01, 03, 15),
+ ),
+ equals('flutter-engine/2020/01/03/15/$kEngineRevision2/values.json'),
+ );
+ },
+ skip: testBucket == null,
+ );
+
+ test('SkiaPerfDestination correctly updates points', () async {
+ final SkiaPerfGcsAdaptor mockGcs = MockSkiaPerfGcsAdaptor();
+ final GcsLock mockLock = MockGcsLock();
+ final SkiaPerfDestination dst = SkiaPerfDestination(mockGcs, mockLock);
+ await dst.update(<MetricPoint>[cocoonPointRev1Metric1],
+ DateTime.fromMillisecondsSinceEpoch(123));
+ await dst.update(<MetricPoint>[cocoonPointRev1Metric2],
+ DateTime.fromMillisecondsSinceEpoch(123));
+ List<SkiaPerfPoint> points = await mockGcs.readPoints(
+ await SkiaPerfGcsAdaptor.computeObjectName(kFlutterFrameworkRepo,
+ kFrameworkRevision1, DateTime.fromMillisecondsSinceEpoch(123)));
+ expect(points.length, equals(2));
+ expectSetMatch(
+ points.map((SkiaPerfPoint p) => p.testName), <String>[kTaskName]);
+ expectSetMatch(points.map((SkiaPerfPoint p) => p.subResult),
+ <String>[kMetric1, kMetric2]);
+ expectSetMatch(
+ points.map((SkiaPerfPoint p) => p.value), <double>[kValue1, kValue2]);
+
+ final MetricPoint updated =
+ MetricPoint(kValue3, cocoonPointRev1Metric1.tags);
+
+ await dst.update(<MetricPoint>[updated, cocoonPointRev2Metric1],
+ DateTime.fromMillisecondsSinceEpoch(123));
+
+ points = await mockGcs.readPoints(
+ await SkiaPerfGcsAdaptor.computeObjectName(kFlutterFrameworkRepo,
+ kFrameworkRevision2, DateTime.fromMillisecondsSinceEpoch(123)));
+ expect(points.length, equals(1));
+ expect(points[0].gitHash, equals(kFrameworkRevision2));
+ expect(points[0].value, equals(kValue3));
+
+ points = await mockGcs.readPoints(
+ await SkiaPerfGcsAdaptor.computeObjectName(kFlutterFrameworkRepo,
+ kFrameworkRevision1, DateTime.fromMillisecondsSinceEpoch(123)));
+ expectSetMatch(
+ points.map((SkiaPerfPoint p) => p.value), <double>[kValue2, kValue3]);
+ });
+
+ Future<void> skiaPerfDestinationIntegrationTest() async {
+ final SkiaPerfDestination destination =
+ SkiaPerfDestination(SkiaPerfGcsAdaptor(testBucket), testLock);
+ await destination.update(<MetricPoint>[cocoonPointRev1Metric1],
+ DateTime.fromMillisecondsSinceEpoch(123));
+ }
+
+ test(
+ 'SkiaPerfDestination integration test',
+ skiaPerfDestinationIntegrationTest,
+ skip: testBucket == null,
+ );
+}
diff --git a/packages/metrics_center/test/utility.dart b/packages/metrics_center/test/utility.dart
new file mode 100644
index 0000000..6dacd48
--- /dev/null
+++ b/packages/metrics_center/test/utility.dart
@@ -0,0 +1,23 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'common.dart';
+
+// This will be used in many of our unit tests.
+void expectSetMatch<T>(Iterable<T> actual, Iterable<T> expected) {
+ expect(Set<T>.from(actual), equals(Set<T>.from(expected)));
+}
+
+// May return null if the credentials file doesn't exist.
+Map<String, dynamic> getTestGcpCredentialsJson() {
+ final File f = File('secret/test_gcp_credentials.json');
+ if (!f.existsSync()) {
+ return null;
+ }
+ return jsonDecode(File('secret/test_gcp_credentials.json').readAsStringSync())
+ as Map<String, dynamic>;
+}
diff --git a/packages/multicast_dns/CHANGELOG.md b/packages/multicast_dns/CHANGELOG.md
new file mode 100644
index 0000000..fa64e26
--- /dev/null
+++ b/packages/multicast_dns/CHANGELOG.md
@@ -0,0 +1,22 @@
+## 0.3.0
+
+* Migrate package to null safety.
+
+## 0.2.2
+* Fixes parsing of TXT records. Continues parsing on non-utf8 strings.
+
+## 0.2.1
+* Fixes the handling of packets containing non-utf8 strings.
+
+## 0.2.0
+* Allow configuration of the port and address the mdns query is performed on.
+
+## 0.1.1
+
+* Fixes [flutter/issue/31854](https://github.com/flutter/flutter/issues/31854) where `decodeMDnsResponse` advanced to incorrect code points and ignored some records.
+
+## 0.1.0
+
+* Initial Open Source release.
+* Migrates the dartino-sdk's mDNS client to Dart 2.0 and Flutter's analysis rules
+* Breaks from original Dartino code, as it does not use native libraries for macOS and overhauls the `ResourceRecord` class.
diff --git a/packages/multicast_dns/LICENSE b/packages/multicast_dns/LICENSE
new file mode 100644
index 0000000..73e6b6e
--- /dev/null
+++ b/packages/multicast_dns/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2019 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/multicast_dns/README.md b/packages/multicast_dns/README.md
new file mode 100644
index 0000000..47f769c
--- /dev/null
+++ b/packages/multicast_dns/README.md
@@ -0,0 +1,21 @@
+# Multicast DNS package
+
+[](
+https://pub.dartlang.org/packages/multicast_dns)
+
+A Dart package to do service discovery over multicast DNS (mDNS), Bonjour, and Avahi.
+
+## Usage
+To use this package, add `multicast_dns` as a
+[dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/).
+
+## Example
+
+Import the library via
+``` dart
+import 'package:multicast_dns/multicast_dns.dart';
+```
+
+Then use the `MDnsClient` Dart class in your code. To see how this is done,
+check out the [example app](example/main.dart) or the sample implementations in
+the [bin](bin/) directory.
diff --git a/packages/multicast_dns/example/main.dart b/packages/multicast_dns/example/main.dart
new file mode 100644
index 0000000..d15f665
--- /dev/null
+++ b/packages/multicast_dns/example/main.dart
@@ -0,0 +1,37 @@
+// Copyright 2018, 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.
+
+// Example script to illustrate how to use the mdns package to discover the port
+// of a Dart observatory over mDNS.
+
+import 'package:multicast_dns/multicast_dns.dart';
+
+Future<void> main() async {
+ // Parse the command line arguments.
+
+ const String name = '_dartobservatory._tcp.local';
+ final MDnsClient client = MDnsClient();
+ // Start the client with default options.
+ await client.start();
+
+ // Get the PTR record for the service.
+ await for (final PtrResourceRecord ptr in client
+ .lookup<PtrResourceRecord>(ResourceRecordQuery.serverPointer(name))) {
+ // Use the domainName from the PTR record to get the SRV record,
+ // which will have the port and local hostname.
+ // Note that duplicate messages may come through, especially if any
+ // other mDNS queries are running elsewhere on the machine.
+ await for (final SrvResourceRecord srv in client.lookup<SrvResourceRecord>(
+ ResourceRecordQuery.service(ptr.domainName))) {
+ // Domain name will be something like "io.flutter.example@some-iphone.local._dartobservatory._tcp.local"
+ final String bundleId =
+ ptr.domainName; //.substring(0, ptr.domainName.indexOf('@'));
+ print('Dart observatory instance found at '
+ '${srv.target}:${srv.port} for "$bundleId".');
+ }
+ }
+ client.stop();
+
+ print('Done.');
+}
diff --git a/packages/multicast_dns/example/mdns_resolve.dart b/packages/multicast_dns/example/mdns_resolve.dart
new file mode 100644
index 0000000..13441d6
--- /dev/null
+++ b/packages/multicast_dns/example/mdns_resolve.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2015, the Dartino 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.
+
+// Example script to illustrate how to use the mdns package to lookup names
+// on the local network.
+
+import 'package:multicast_dns/multicast_dns.dart';
+
+Future<void> main(List<String> args) async {
+ if (args.length != 1) {
+ print('''
+Please provide an address as argument.
+
+For example:
+ dart mdns_resolve.dart dartino.local''');
+ return;
+ }
+
+ final String name = args[0];
+
+ final MDnsClient client = MDnsClient();
+ await client.start();
+ await for (final IPAddressResourceRecord record in client
+ .lookup<IPAddressResourceRecord>(ResourceRecordQuery.addressIPv4(name))) {
+ print('Found address (${record.address}).');
+ }
+
+ await for (final IPAddressResourceRecord record in client
+ .lookup<IPAddressResourceRecord>(ResourceRecordQuery.addressIPv6(name))) {
+ print('Found address (${record.address}).');
+ }
+ client.stop();
+}
diff --git a/packages/multicast_dns/example/mdns_sd.dart b/packages/multicast_dns/example/mdns_sd.dart
new file mode 100644
index 0000000..b4d05ad
--- /dev/null
+++ b/packages/multicast_dns/example/mdns_sd.dart
@@ -0,0 +1,61 @@
+// Copyright (c) 2015, the Dartino 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.
+
+// Example script to illustrate how to use the mdns package to discover services
+// on the local network.
+
+import 'package:multicast_dns/multicast_dns.dart';
+
+Future<void> main(List<String> args) async {
+ if (args.isEmpty) {
+ print('''
+Please provide the name of a service as argument.
+
+For example:
+ dart mdns_sd.dart [--verbose] _workstation._tcp.local''');
+ return;
+ }
+
+ final bool verbose = args.contains('--verbose') || args.contains('-v');
+ final String name = args.last;
+ final MDnsClient client = MDnsClient();
+ await client.start();
+
+ await for (final PtrResourceRecord ptr in client
+ .lookup<PtrResourceRecord>(ResourceRecordQuery.serverPointer(name))) {
+ if (verbose) {
+ print(ptr);
+ }
+ await for (final SrvResourceRecord srv in client.lookup<SrvResourceRecord>(
+ ResourceRecordQuery.service(ptr.domainName))) {
+ if (verbose) {
+ print(srv);
+ }
+ if (verbose) {
+ await client
+ .lookup<TxtResourceRecord>(ResourceRecordQuery.text(ptr.domainName))
+ .forEach(print);
+ }
+ await for (final IPAddressResourceRecord ip
+ in client.lookup<IPAddressResourceRecord>(
+ ResourceRecordQuery.addressIPv4(srv.target))) {
+ if (verbose) {
+ print(ip);
+ }
+ print('Service instance found at '
+ '${srv.target}:${srv.port} with ${ip.address}.');
+ }
+ await for (final IPAddressResourceRecord ip
+ in client.lookup<IPAddressResourceRecord>(
+ ResourceRecordQuery.addressIPv6(srv.target))) {
+ if (verbose) {
+ print(ip);
+ }
+ print('Service instance found at '
+ '${srv.target}:${srv.port} with ${ip.address}.');
+ }
+ }
+ }
+ client.stop();
+}
diff --git a/packages/multicast_dns/lib/multicast_dns.dart b/packages/multicast_dns/lib/multicast_dns.dart
new file mode 100644
index 0000000..0b014cf
--- /dev/null
+++ b/packages/multicast_dns/lib/multicast_dns.dart
@@ -0,0 +1,235 @@
+// Copyright (c) 2015, the Dartino 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:multicast_dns/src/constants.dart';
+import 'package:multicast_dns/src/lookup_resolver.dart';
+import 'package:multicast_dns/src/native_protocol_client.dart';
+import 'package:multicast_dns/src/packet.dart';
+import 'package:multicast_dns/src/resource_record.dart';
+
+export 'package:multicast_dns/src/resource_record.dart';
+
+/// A callback type for [MDnsQuerier.start] to iterate available network
+/// interfaces.
+///
+/// Implementations must ensure they return interfaces appropriate for the
+/// [type] parameter.
+///
+/// See also:
+/// * [MDnsQuerier.allInterfacesFactory]
+typedef NetworkInterfacesFactory = Future<Iterable<NetworkInterface>> Function(
+ InternetAddressType type);
+
+/// A factory for construction of datagram sockets.
+///
+/// This can be injected into the [MDnsClient] to provide alternative
+/// implementations of [RawDatagramSocket.bind].
+typedef RawDatagramSocketFactory = Future<RawDatagramSocket> Function(
+ dynamic host, int port,
+ {bool reuseAddress, bool reusePort, int ttl});
+
+/// Client for DNS lookup and publishing using the mDNS protocol.
+///
+/// Users should call [MDnsQuerier.start] when ready to start querying and
+/// listening. [MDnsQuerier.stop] must be called when done to clean up
+/// resources.
+///
+/// This client only supports "One-Shot Multicast DNS Queries" as described in
+/// section 5.1 of [RFC 6762](https://tools.ietf.org/html/rfc6762).
+class MDnsClient {
+ /// Create a new [MDnsClient].
+ MDnsClient({
+ RawDatagramSocketFactory rawDatagramSocketFactory = RawDatagramSocket.bind,
+ }) : _rawDatagramSocketFactory = rawDatagramSocketFactory;
+
+ bool _starting = false;
+ bool _started = false;
+ final List<RawDatagramSocket> _sockets = <RawDatagramSocket>[];
+ final LookupResolver _resolver = LookupResolver();
+ final ResourceRecordCache _cache = ResourceRecordCache();
+ final RawDatagramSocketFactory _rawDatagramSocketFactory;
+
+ InternetAddress? _mDnsAddress;
+ int? _mDnsPort;
+
+ /// Find all network interfaces with an the [InternetAddressType] specified.
+ Future<Iterable<NetworkInterface>> allInterfacesFactory(
+ InternetAddressType type) {
+ return NetworkInterface.list(
+ includeLinkLocal: true,
+ type: type,
+ includeLoopback: true,
+ );
+ }
+
+ /// Start the mDNS client.
+ ///
+ /// With no arguments, this method will listen on the IPv4 multicast address
+ /// on all IPv4 network interfaces.
+ ///
+ /// The [listenAddress] parameter must be either [InternetAddress.anyIPv4] or
+ /// [InternetAddress.anyIPv6], and will default to anyIPv4.
+ ///
+ /// The [interfaceFactory] defaults to [allInterfacesFactory].
+ ///
+ /// The [mDnsPort] allows configuring what port is used for the mDNS
+ /// query. If not provided, defaults to `5353`.
+ ///
+ /// The [mDnsAddress] allows configuring what internet address is used
+ /// for the mDNS query. If not provided, defaults to either `224.0.0.251` or
+ /// or `FF02::FB`.
+ Future<void> start({
+ InternetAddress? listenAddress,
+ NetworkInterfacesFactory? interfacesFactory,
+ int mDnsPort = mDnsPort,
+ InternetAddress? mDnsAddress,
+ }) async {
+ listenAddress ??= InternetAddress.anyIPv4;
+ interfacesFactory ??= allInterfacesFactory;
+ final int selectedMDnsPort = _mDnsPort = mDnsPort;
+ _mDnsAddress = mDnsAddress;
+
+ assert(listenAddress.address == InternetAddress.anyIPv4.address ||
+ listenAddress.address == InternetAddress.anyIPv6.address);
+
+ if (_started || _starting) {
+ return;
+ }
+ _starting = true;
+
+ // Listen on all addresses.
+ final RawDatagramSocket incoming = await _rawDatagramSocketFactory(
+ listenAddress.address,
+ selectedMDnsPort,
+ reuseAddress: true,
+ reusePort: true,
+ ttl: 255,
+ );
+
+ // Can't send to IPv6 any address.
+ if (incoming.address != InternetAddress.anyIPv6) {
+ _sockets.add(incoming);
+ }
+
+ _mDnsAddress ??= incoming.address.type == InternetAddressType.IPv4
+ ? mDnsAddressIPv4
+ : mDnsAddressIPv6;
+
+ final List<NetworkInterface> interfaces =
+ (await interfacesFactory(listenAddress.type)).toList();
+
+ for (final NetworkInterface interface in interfaces) {
+ // Create a socket for sending on each adapter.
+ final InternetAddress targetAddress = interface.addresses[0];
+ final RawDatagramSocket socket = await _rawDatagramSocketFactory(
+ targetAddress,
+ selectedMDnsPort,
+ reuseAddress: true,
+ reusePort: true,
+ ttl: 255,
+ );
+ _sockets.add(socket);
+ // Ensure that we're using this address/interface for multicast.
+ if (targetAddress.type == InternetAddressType.IPv4) {
+ socket.setRawOption(RawSocketOption(
+ RawSocketOption.levelIPv4,
+ RawSocketOption.IPv4MulticastInterface,
+ targetAddress.rawAddress,
+ ));
+ } else {
+ socket.setRawOption(RawSocketOption.fromInt(
+ RawSocketOption.levelIPv6,
+ RawSocketOption.IPv6MulticastInterface,
+ interface.index,
+ ));
+ }
+ // Join multicast on this interface.
+ incoming.joinMulticast(_mDnsAddress!, interface);
+ }
+ incoming.listen((RawSocketEvent event) => _handleIncoming(event, incoming));
+ _started = true;
+ _starting = false;
+ }
+
+ /// Stop the client and close any associated sockets.
+ void stop() {
+ if (!_started) {
+ return;
+ }
+ if (_starting) {
+ throw StateError('Cannot stop mDNS client while it is starting.');
+ }
+
+ for (final RawDatagramSocket socket in _sockets) {
+ socket.close();
+ }
+
+ _resolver.clearPendingRequests();
+
+ _started = false;
+ }
+
+ /// Lookup a [ResourceRecord], potentially from the cache.
+ ///
+ /// The [type] parameter must be a valid [ResourceRecordType]. The [fullyQualifiedName]
+ /// parameter is the name of the service to lookup, and must not be null. The
+ /// [timeout] parameter specifies how long the internal cache should hold on
+ /// to the record. The [multicast] parameter specifies whether the query
+ /// should be sent as unicast (QU) or multicast (QM).
+ ///
+ /// Some publishers have been observed to not respond to unicast requests
+ /// properly, so the default is true.
+ Stream<T> lookup<T extends ResourceRecord>(
+ ResourceRecordQuery query, {
+ Duration timeout = const Duration(seconds: 5),
+ }) {
+ final int? selectedMDnsPort = _mDnsPort;
+ if (!_started || selectedMDnsPort == null) {
+ throw StateError('mDNS client must be started before calling lookup.');
+ }
+ // Look for entries in the cache.
+ final List<T> cached = <T>[];
+ _cache.lookup<T>(
+ query.fullyQualifiedName, query.resourceRecordType, cached);
+ if (cached.isNotEmpty) {
+ final StreamController<T> controller = StreamController<T>();
+ cached.forEach(controller.add);
+ controller.close();
+ return controller.stream;
+ }
+
+ // Add the pending request before sending the query.
+ final Stream<T> results = _resolver.addPendingRequest<T>(
+ query.resourceRecordType, query.fullyQualifiedName, timeout);
+
+ // Send the request on all interfaces.
+ final List<int> packet = query.encode();
+ for (final RawDatagramSocket socket in _sockets) {
+ socket.send(packet, _mDnsAddress!, selectedMDnsPort);
+ }
+ return results;
+ }
+
+ // Process incoming datagrams.
+ void _handleIncoming(RawSocketEvent event, RawDatagramSocket incoming) {
+ if (event == RawSocketEvent.read) {
+ final Datagram? datagram = incoming.receive();
+ if (datagram == null) {
+ return;
+ }
+
+ // Check for published responses.
+ final List<ResourceRecord>? response = decodeMDnsResponse(datagram.data);
+ if (response != null) {
+ _cache.updateRecords(response);
+ _resolver.handleResponse(response);
+ return;
+ }
+ // TODO(dnfield): Support queries coming in for published entries.
+ }
+ }
+}
diff --git a/packages/multicast_dns/lib/src/constants.dart b/packages/multicast_dns/lib/src/constants.dart
new file mode 100644
index 0000000..061329d
--- /dev/null
+++ b/packages/multicast_dns/lib/src/constants.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2015, the Dartino 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';
+
+/// The IPv4 mDNS Address.
+final InternetAddress mDnsAddressIPv4 = InternetAddress('224.0.0.251');
+
+/// The IPv6 mDNS Address.
+final InternetAddress mDnsAddressIPv6 = InternetAddress('FF02::FB');
+
+/// The mDNS port.
+const int mDnsPort = 5353;
+
+/// Enumeration of supported resource record class types.
+abstract class ResourceRecordClass {
+ // This class is intended to be used as a namespace, and should not be
+ // extended directly.
+ ResourceRecordClass._();
+
+ /// Internet address class ("IN").
+ static const int internet = 1;
+}
+
+/// Enumeration of DNS question types.
+abstract class QuestionType {
+ // This class is intended to be used as a namespace, and should not be
+ // extended directly.
+ QuestionType._();
+
+ /// "QU" Question.
+ static const int unicast = 0x8000;
+
+ /// "QM" Question.
+ static const int multicast = 0x0000;
+}
diff --git a/packages/multicast_dns/lib/src/lookup_resolver.dart b/packages/multicast_dns/lib/src/lookup_resolver.dart
new file mode 100644
index 0000000..a25e691
--- /dev/null
+++ b/packages/multicast_dns/lib/src/lookup_resolver.dart
@@ -0,0 +1,96 @@
+// Copyright (c) 2015, the Dartino 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:collection';
+
+import 'package:multicast_dns/src/resource_record.dart';
+
+/// Class for maintaining state about pending mDNS requests.
+class PendingRequest extends LinkedListEntry<PendingRequest> {
+ /// Creates a new PendingRequest.
+ PendingRequest(this.type, this.domainName, this.controller);
+
+ /// The [ResourceRecordType] of the request.
+ final int type;
+
+ /// The domain name to look up via mDNS.
+ ///
+ /// For example, `'_http._tcp.local` to look up HTTP services on the local
+ /// domain.
+ final String domainName;
+
+ /// A StreamController managing the request.
+ final StreamController<ResourceRecord> controller;
+
+ /// The timer for the request.
+ Timer? timer;
+}
+
+/// Class for keeping track of pending lookups and processing incoming
+/// query responses.
+class LookupResolver {
+ final LinkedList<PendingRequest> _pendingRequests =
+ LinkedList<PendingRequest>();
+
+ /// Adds a request and returns a [Stream] of [ResourceRecord] responses.
+ Stream<T> addPendingRequest<T extends ResourceRecord>(
+ int type, String name, Duration timeout) {
+ final StreamController<T> controller = StreamController<T>();
+ final PendingRequest request = PendingRequest(type, name, controller);
+ final Timer timer = Timer(timeout, () {
+ request.unlink();
+ controller.close();
+ });
+ request.timer = timer;
+ _pendingRequests.add(request);
+ return controller.stream;
+ }
+
+ /// Parses [ResoureRecord]s received and delivers them to the appropriate
+ /// listener(s) added via [addPendingRequest].
+ void handleResponse(List<ResourceRecord> response) {
+ for (final ResourceRecord r in response) {
+ final int type = r.resourceRecordType;
+ String name = r.name.toLowerCase();
+ if (name.endsWith('.')) {
+ name = name.substring(0, name.length - 1);
+ }
+
+ bool responseMatches(PendingRequest request) {
+ String requestName = request.domainName.toLowerCase();
+ // make, e.g. "_http" become "_http._tcp.local".
+ if (!requestName.endsWith('local')) {
+ if (!requestName.endsWith('._tcp.local') &&
+ !requestName.endsWith('._udp.local') &&
+ !requestName.endsWith('._tcp') &&
+ !requestName.endsWith('.udp')) {
+ requestName += '._tcp';
+ }
+ requestName += '.local';
+ }
+ return requestName == name && request.type == type;
+ }
+
+ for (final PendingRequest pendingRequest in _pendingRequests) {
+ if (responseMatches(pendingRequest)) {
+ if (pendingRequest.controller.isClosed) {
+ return;
+ }
+ pendingRequest.controller.add(r);
+ }
+ }
+ }
+ }
+
+ /// Removes any pending requests and ends processing.
+ void clearPendingRequests() {
+ while (_pendingRequests.isNotEmpty) {
+ final PendingRequest request = _pendingRequests.first;
+ request.unlink();
+ request.timer?.cancel();
+ request.controller.close();
+ }
+ }
+}
diff --git a/packages/multicast_dns/lib/src/native_protocol_client.dart b/packages/multicast_dns/lib/src/native_protocol_client.dart
new file mode 100644
index 0000000..15e70c9
--- /dev/null
+++ b/packages/multicast_dns/lib/src/native_protocol_client.dart
@@ -0,0 +1,79 @@
+// Copyright (c) 2015, the Dartino 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:collection';
+
+import 'package:multicast_dns/src/resource_record.dart';
+
+/// Cache for resource records that have been received.
+///
+/// There can be multiple entries for the same name and type.
+///
+/// The cache is updated with a list of records, because it needs to remove
+/// all entries that correspond to the name and type of the name/type
+/// combinations of records that should be updated. For example, a host may
+/// remove one of its IP addresses and report the remaining address as a
+/// response - then we need to clear all previous entries for that host before
+/// updating the cache.
+class ResourceRecordCache {
+ /// Creates a new ResourceRecordCache.
+ ResourceRecordCache();
+
+ final Map<int, SplayTreeMap<String, List<ResourceRecord>>> _cache =
+ <int, SplayTreeMap<String, List<ResourceRecord>>>{};
+
+ /// The number of entries in the cache.
+ int get entryCount {
+ int count = 0;
+ for (final SplayTreeMap<String, List<ResourceRecord>> map
+ in _cache.values) {
+ for (final List<ResourceRecord> records in map.values) {
+ count += records.length;
+ }
+ }
+ return count;
+ }
+
+ /// Update the records in this cache.
+ void updateRecords(List<ResourceRecord> records) {
+ // TODO(karlklose): include flush bit in the record and only flush if
+ // necessary.
+ // Clear the cache for all name/type combinations to be updated.
+ final Map<int, Set<String>> seenRecordTypes = <int, Set<String>>{};
+ for (final ResourceRecord record in records) {
+ // TODO(dnfield): Update this to use set literal syntax when we're able to bump the SDK constraint.
+ seenRecordTypes[record.resourceRecordType] ??=
+ Set<String>(); // ignore: prefer_collection_literals
+ if (seenRecordTypes[record.resourceRecordType]!.add(record.name)) {
+ _cache[record.resourceRecordType] ??=
+ SplayTreeMap<String, List<ResourceRecord>>();
+
+ _cache[record.resourceRecordType]![record.name] = <ResourceRecord>[
+ record
+ ];
+ } else {
+ _cache[record.resourceRecordType]![record.name]!.add(record);
+ }
+ }
+ }
+
+ /// Get a record from this cache.
+ void lookup<T extends ResourceRecord>(
+ String name, int type, List<T> results) {
+ assert(ResourceRecordType.debugAssertValid(type));
+ final int time = DateTime.now().millisecondsSinceEpoch;
+ final SplayTreeMap<String, List<ResourceRecord>>? candidates = _cache[type];
+ if (candidates == null) {
+ return;
+ }
+
+ final List<ResourceRecord>? candidateRecords = candidates[name];
+ if (candidateRecords == null) {
+ return;
+ }
+ candidateRecords
+ .removeWhere((ResourceRecord candidate) => candidate.validUntil < time);
+ results.addAll(candidateRecords.cast<T>());
+ }
+}
diff --git a/packages/multicast_dns/lib/src/packet.dart b/packages/multicast_dns/lib/src/packet.dart
new file mode 100644
index 0000000..9905689
--- /dev/null
+++ b/packages/multicast_dns/lib/src/packet.dart
@@ -0,0 +1,397 @@
+// Copyright (c) 2015, the Dart 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:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:multicast_dns/src/constants.dart';
+import 'package:multicast_dns/src/resource_record.dart';
+
+// Offsets into the header. See https://tools.ietf.org/html/rfc1035.
+const int _kIdOffset = 0;
+const int _kFlagsOffset = 2;
+const int _kQdcountOffset = 4;
+const int _kAncountOffset = 6;
+const int _kNscountOffset = 8;
+const int _kArcountOffset = 10;
+const int _kHeaderSize = 12;
+
+/// Processes a DNS query name into a list of parts.
+///
+/// Will attempt to append 'local' if the name is something like '_http._tcp',
+/// and '._tcp.local' if name is something like '_http'.
+List<String> processDnsNameParts(String name) {
+ final List<String> parts = name.split('.');
+ if (parts.length == 1) {
+ return <String>[parts[0], '_tcp', 'local'];
+ } else if (parts.length == 2 && parts[1].startsWith('_')) {
+ return <String>[parts[0], parts[1], 'local'];
+ }
+
+ return parts;
+}
+
+/// Encode an mDNS query packet.
+///
+/// The [type] parameter must be a valid [ResourceRecordType] value. The
+/// [multicast] parameter must not be null.
+///
+/// This is a low level API; most consumers should prefer
+/// [ResourceRecordQuery.encode], which offers some convenience wrappers around
+/// selecting the correct [type] and setting the [name] parameter correctly.
+List<int> encodeMDnsQuery(
+ String name, {
+ int type = ResourceRecordType.addressIPv4,
+ bool multicast = true,
+}) {
+ assert(ResourceRecordType.debugAssertValid(type));
+
+ final List<String> nameParts = processDnsNameParts(name);
+ final List<List<int>> rawNameParts =
+ nameParts.map<List<int>>((String part) => utf8.encode(part)).toList();
+
+ // Calculate the size of the packet.
+ int size = _kHeaderSize;
+ for (int i = 0; i < rawNameParts.length; i++) {
+ size += 1 + rawNameParts[i].length;
+ }
+
+ size += 1; // End with empty part
+ size += 4; // Trailer (QTYPE and QCLASS).
+ final Uint8List data = Uint8List(size);
+ final ByteData packetByteData = ByteData.view(data.buffer);
+ // Query identifier - just use 0.
+ packetByteData.setUint16(_kIdOffset, 0);
+ // Flags - 0 for query.
+ packetByteData.setUint16(_kFlagsOffset, 0);
+ // Query count.
+ packetByteData.setUint16(_kQdcountOffset, 1);
+ // Number of answers - 0 for query.
+ packetByteData.setUint16(_kAncountOffset, 0);
+ // Number of name server records - 0 for query.
+ packetByteData.setUint16(_kNscountOffset, 0);
+ // Number of resource records - 0 for query.
+ packetByteData.setUint16(_kArcountOffset, 0);
+ int offset = _kHeaderSize;
+ for (int i = 0; i < rawNameParts.length; i++) {
+ data[offset++] = rawNameParts[i].length;
+ data.setRange(offset, offset + rawNameParts[i].length, rawNameParts[i]);
+ offset += rawNameParts[i].length;
+ }
+
+ data[offset] = 0; // Empty part.
+ offset++;
+ packetByteData.setUint16(offset, type); // QTYPE.
+ offset += 2;
+ packetByteData.setUint16(
+ offset,
+ ResourceRecordClass.internet |
+ (multicast ? QuestionType.multicast : QuestionType.unicast));
+
+ return data;
+}
+
+/// Result of reading a Fully Qualified Domain Name (FQDN).
+class _FQDNReadResult {
+ /// Creates a new FQDN read result.
+ _FQDNReadResult(this.fqdnParts, this.bytesRead);
+
+ /// The raw parts of the FQDN.
+ final List<String> fqdnParts;
+
+ /// The bytes consumed from the packet for this FQDN.
+ final int bytesRead;
+
+ /// Returns the Fully Qualified Domain Name.
+ String get fqdn => fqdnParts.join('.');
+
+ @override
+ String toString() => fqdn;
+}
+
+/// Reads a FQDN from raw packet data.
+String readFQDN(List<int> packet, [int offset = 0]) {
+ final Uint8List data =
+ packet is Uint8List ? packet : Uint8List.fromList(packet);
+ final ByteData byteData = ByteData.view(data.buffer);
+
+ return _readFQDN(data, byteData, offset, data.length).fqdn;
+}
+
+// Read a FQDN at the given offset. Returns a pair with the FQDN
+// parts and the number of bytes consumed.
+//
+// If decoding fails (e.g. due to an invalid packet) `null` is returned.
+_FQDNReadResult _readFQDN(
+ Uint8List data, ByteData byteData, int offset, int length) {
+ void checkLength(int required) {
+ if (length < required) {
+ throw MDnsDecodeException(required);
+ }
+ }
+
+ final List<String> parts = <String>[];
+ final int prevOffset = offset;
+ while (true) {
+ // At least one byte is required.
+ checkLength(offset + 1);
+
+ // Check for compressed.
+ if (data[offset] & 0xc0 == 0xc0) {
+ // At least two bytes are required for a compressed FQDN.
+ checkLength(offset + 2);
+
+ // A compressed FQDN has a new offset in the lower 14 bits.
+ final _FQDNReadResult result = _readFQDN(
+ data, byteData, byteData.getUint16(offset) & ~0xc000, length);
+ parts.addAll(result.fqdnParts);
+ offset += 2;
+ break;
+ } else {
+ // A normal FQDN part has a length and a UTF-8 encoded name
+ // part. If the length is 0 this is the end of the FQDN.
+ final int partLength = data[offset];
+ offset++;
+ if (partLength > 0) {
+ checkLength(offset + partLength);
+ final Uint8List partBytes =
+ Uint8List.view(data.buffer, offset, partLength);
+ offset += partLength;
+ // According to the RFC, this is supposed to be utf-8 encoded, but
+ // we should continue decoding even if it isn't to avoid dropping the
+ // rest of the data, which might still be useful.
+ parts.add(utf8.decode(partBytes, allowMalformed: true));
+ } else {
+ break;
+ }
+ }
+ }
+ return _FQDNReadResult(parts, offset - prevOffset);
+}
+
+/// Decode an mDNS query packet.
+///
+/// If decoding fails (e.g. due to an invalid packet), `null` is returned.
+///
+/// See https://tools.ietf.org/html/rfc1035 for format.
+ResourceRecordQuery? decodeMDnsQuery(List<int> packet) {
+ final int length = packet.length;
+ if (length < _kHeaderSize) {
+ return null;
+ }
+
+ final Uint8List data =
+ packet is Uint8List ? packet : Uint8List.fromList(packet);
+ final ByteData packetBytes = ByteData.view(data.buffer);
+
+ // Check whether it's a query.
+ final int flags = packetBytes.getUint16(_kFlagsOffset);
+ if (flags != 0) {
+ return null;
+ }
+ final int questionCount = packetBytes.getUint16(_kQdcountOffset);
+ if (questionCount == 0) {
+ return null;
+ }
+
+ final _FQDNReadResult fqdn =
+ _readFQDN(data, packetBytes, _kHeaderSize, data.length);
+
+ int offset = _kHeaderSize + fqdn.bytesRead;
+ final int type = packetBytes.getUint16(offset);
+ offset += 2;
+ final int queryType = packetBytes.getUint16(offset) & 0x8000;
+ return ResourceRecordQuery(type, fqdn.fqdn, queryType);
+}
+
+/// Decode an mDNS response packet.
+///
+/// If decoding fails (e.g. due to an invalid packet) `null` is returned.
+///
+/// See https://tools.ietf.org/html/rfc1035 for the format.
+List<ResourceRecord>? decodeMDnsResponse(List<int> packet) {
+ final int length = packet.length;
+ if (length < _kHeaderSize) {
+ return null;
+ }
+
+ final Uint8List data =
+ packet is Uint8List ? packet : Uint8List.fromList(packet);
+ final ByteData packetBytes = ByteData.view(data.buffer);
+
+ final int answerCount = packetBytes.getUint16(_kAncountOffset);
+ final int authorityCount = packetBytes.getUint16(_kNscountOffset);
+ final int additionalCount = packetBytes.getUint16(_kArcountOffset);
+ final int remainingCount = answerCount + authorityCount + additionalCount;
+
+ if (remainingCount == 0) {
+ return null;
+ }
+
+ final int questionCount = packetBytes.getUint16(_kQdcountOffset);
+ int offset = _kHeaderSize;
+
+ void checkLength(int required) {
+ if (length < required) {
+ throw MDnsDecodeException(required);
+ }
+ }
+
+ ResourceRecord? readResourceRecord() {
+ // First read the FQDN.
+ final _FQDNReadResult result = _readFQDN(data, packetBytes, offset, length);
+ final String fqdn = result.fqdn;
+ offset += result.bytesRead;
+ checkLength(offset + 2);
+ final int type = packetBytes.getUint16(offset);
+ offset += 2;
+ // The first bit of the rrclass field is set to indicate that the answer is
+ // unique and the querier should flush the cached answer for this name
+ // (RFC 6762, Sec. 10.2). We ignore it for now since we don't cache answers.
+ checkLength(offset + 2);
+ final int resourceRecordClass = packetBytes.getUint16(offset) & 0x7fff;
+
+ if (resourceRecordClass != ResourceRecordClass.internet) {
+ // We do not support other classes.
+ return null;
+ }
+
+ offset += 2;
+ checkLength(offset + 4);
+ final int ttl = packetBytes.getInt32(offset);
+ offset += 4;
+
+ checkLength(offset + 2);
+ final int readDataLength = packetBytes.getUint16(offset);
+ offset += 2;
+ final int validUntil = DateTime.now().millisecondsSinceEpoch + ttl * 1000;
+ switch (type) {
+ case ResourceRecordType.addressIPv4:
+ checkLength(offset + readDataLength);
+ final StringBuffer addr = StringBuffer();
+ final int stop = offset + readDataLength;
+ addr.write(packetBytes.getUint8(offset));
+ offset++;
+ for (; offset < stop; offset++) {
+ addr.write('.');
+ addr.write(packetBytes.getUint8(offset));
+ }
+ return IPAddressResourceRecord(fqdn, validUntil,
+ address: InternetAddress(addr.toString()));
+ case ResourceRecordType.addressIPv6:
+ checkLength(offset + readDataLength);
+ final StringBuffer addr = StringBuffer();
+ final int stop = offset + readDataLength;
+ addr.write(packetBytes.getUint16(offset).toRadixString(16));
+ offset += 2;
+ for (; offset < stop; offset += 2) {
+ addr.write(':');
+ addr.write(packetBytes.getUint16(offset).toRadixString(16));
+ }
+ return IPAddressResourceRecord(
+ fqdn,
+ validUntil,
+ address: InternetAddress(addr.toString()),
+ );
+ case ResourceRecordType.service:
+ checkLength(offset + 2);
+ final int priority = packetBytes.getUint16(offset);
+ offset += 2;
+ checkLength(offset + 2);
+ final int weight = packetBytes.getUint16(offset);
+ offset += 2;
+ checkLength(offset + 2);
+ final int port = packetBytes.getUint16(offset);
+ offset += 2;
+ final _FQDNReadResult result =
+ _readFQDN(data, packetBytes, offset, length);
+ offset += result.bytesRead;
+ return SrvResourceRecord(
+ fqdn,
+ validUntil,
+ target: result.fqdn,
+ port: port,
+ priority: priority,
+ weight: weight,
+ );
+ case ResourceRecordType.serverPointer:
+ checkLength(offset + readDataLength);
+ final _FQDNReadResult result =
+ _readFQDN(data, packetBytes, offset, length);
+ offset += readDataLength;
+ return PtrResourceRecord(
+ fqdn,
+ validUntil,
+ domainName: result.fqdn,
+ );
+ case ResourceRecordType.text:
+ checkLength(offset + readDataLength);
+ // The first byte of the buffer is the length of the first string of
+ // the TXT record. Further length-prefixed strings may follow. We
+ // concatenate them with newlines.
+ final StringBuffer strings = StringBuffer();
+ int index = 0;
+ while (index < readDataLength) {
+ final int txtLength = data[offset + index];
+ index++;
+ if (txtLength == 0) {
+ continue;
+ }
+ final String text = utf8.decode(
+ Uint8List.view(data.buffer, offset + index, txtLength),
+ allowMalformed: true,
+ );
+ strings.writeln(text);
+ index += txtLength;
+ }
+ offset += readDataLength;
+ return TxtResourceRecord(fqdn, validUntil, text: strings.toString());
+ default:
+ checkLength(offset + readDataLength);
+ offset += readDataLength;
+ return null;
+ }
+ }
+
+ // This list can't be fixed length right now because we might get
+ // resource record types we don't support, and consumers expect this list
+ // to not have null entries.
+ final List<ResourceRecord> result = <ResourceRecord>[];
+
+ try {
+ for (int i = 0; i < questionCount; i++) {
+ final _FQDNReadResult result =
+ _readFQDN(data, packetBytes, offset, length);
+ offset += result.bytesRead;
+ checkLength(offset + 4);
+ offset += 4;
+ }
+ for (int i = 0; i < remainingCount; i++) {
+ final ResourceRecord? record = readResourceRecord();
+ if (record != null) {
+ result.add(record);
+ }
+ }
+ } on MDnsDecodeException {
+ // If decoding fails return null.
+ return null;
+ }
+ return result;
+}
+
+/// This exception is thrown by the decoder when the packet is invalid.
+class MDnsDecodeException implements Exception {
+ /// Creates a new MDnsDecodeException, indicating an error in decoding at the
+ /// specified [offset].
+ ///
+ /// The [offset] parameter should not be null.
+ const MDnsDecodeException(this.offset);
+
+ /// The offset in the packet at which the exception occurred.
+ final int offset;
+
+ @override
+ String toString() => 'Decoding error at $offset';
+}
diff --git a/packages/multicast_dns/lib/src/resource_record.dart b/packages/multicast_dns/lib/src/resource_record.dart
new file mode 100644
index 0000000..5a752b7
--- /dev/null
+++ b/packages/multicast_dns/lib/src/resource_record.dart
@@ -0,0 +1,394 @@
+// Copyright 2018 The Flutter Authors. All 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 'package:meta/meta.dart';
+import 'package:multicast_dns/src/constants.dart';
+import 'package:multicast_dns/src/packet.dart';
+
+// TODO(dnfield): Probably should go with a real hashing function here
+// when https://github.com/dart-lang/sdk/issues/11617 is figured out.
+const int _seedHashPrime = 2166136261;
+const int _multipleHashPrime = 16777619;
+
+int _combineHash(int current, int hash) =>
+ (current & _multipleHashPrime) ^ hash;
+
+int _hashValues(List<int> values) {
+ assert(values.isNotEmpty);
+
+ return values.fold(
+ _seedHashPrime,
+ (int current, int next) => _combineHash(current, next),
+ );
+}
+
+/// Enumeration of support resource record types.
+abstract class ResourceRecordType {
+ // This class is intended to be used as a namespace, and should not be
+ // extended directly.
+ ResourceRecordType._();
+
+ /// An IPv4 Address record, also known as an "A" record. It has a value of 1.
+ static const int addressIPv4 = 1;
+
+ /// An IPv6 Address record, also known as an "AAAA" record. It has a vaule of
+ /// 28.
+ static const int addressIPv6 = 28;
+
+ /// An IP Address reverse map record, also known as a "PTR" recored. It has a
+ /// value of 12.
+ static const int serverPointer = 12;
+
+ /// An available service record, also known as an "SRV" record. It has a
+ /// value of 33.
+ static const int service = 33;
+
+ /// A text record, also known as a "TXT" record. It has a value of 16.
+ static const int text = 16;
+
+ // TODO(dnfield): Support ANY in some meaningful way. Might be server only.
+ // /// A query for all records of all types known to the name server.
+ // static const int any = 255;
+
+ /// Checks that a given int is a valid ResourceRecordType.
+ ///
+ /// This method is intended to be called only from an `assert()`.
+ static bool debugAssertValid(int resourceRecordType) {
+ return resourceRecordType == addressIPv4 ||
+ resourceRecordType == addressIPv6 ||
+ resourceRecordType == serverPointer ||
+ resourceRecordType == service ||
+ resourceRecordType == text;
+ }
+
+ /// Prints a debug-friendly version of the resource record type value.
+ static String toDebugString(int resourceRecordType) {
+ switch (resourceRecordType) {
+ case addressIPv4:
+ return 'A (IPv4 Address)';
+ case addressIPv6:
+ return 'AAAA (IPv6 Address)';
+ case serverPointer:
+ return 'PTR (Domain Name Pointer)';
+ case service:
+ return 'SRV (Service record)';
+ case text:
+ return 'TXT (Text)';
+ }
+ return 'Unknown ($resourceRecordType)';
+ }
+}
+
+/// Represents a DNS query.
+@immutable
+class ResourceRecordQuery {
+ /// Creates a new ResourceRecordQuery.
+ ///
+ /// Most callers should prefer one of the named constructors.
+ ResourceRecordQuery(
+ this.resourceRecordType,
+ this.fullyQualifiedName,
+ this.questionType,
+ ) : assert(ResourceRecordType.debugAssertValid(resourceRecordType));
+
+ /// An A (IPv4) query.
+ ResourceRecordQuery.addressIPv4(
+ String name, {
+ bool isMulticast = true,
+ }) : this(
+ ResourceRecordType.addressIPv4,
+ name,
+ isMulticast ? QuestionType.multicast : QuestionType.unicast,
+ );
+
+ /// An AAAA (IPv6) query.
+ ResourceRecordQuery.addressIPv6(
+ String name, {
+ bool isMulticast = true,
+ }) : this(
+ ResourceRecordType.addressIPv6,
+ name,
+ isMulticast ? QuestionType.multicast : QuestionType.unicast,
+ );
+
+ /// A PTR (Server pointer) query.
+ ResourceRecordQuery.serverPointer(
+ String name, {
+ bool isMulticast = true,
+ }) : this(
+ ResourceRecordType.serverPointer,
+ name,
+ isMulticast ? QuestionType.multicast : QuestionType.unicast,
+ );
+
+ /// An SRV (Service) query.
+ ResourceRecordQuery.service(
+ String name, {
+ bool isMulticast = true,
+ }) : this(
+ ResourceRecordType.service,
+ name,
+ isMulticast ? QuestionType.multicast : QuestionType.unicast,
+ );
+
+ /// A TXT (Text record) query.
+ ResourceRecordQuery.text(
+ String name, {
+ bool isMulticast = true,
+ }) : this(
+ ResourceRecordType.text,
+ name,
+ isMulticast ? QuestionType.multicast : QuestionType.unicast,
+ );
+
+ /// Tye type of resource record - one of [ResourceRecordType]'s values.
+ final int resourceRecordType;
+
+ /// The Fully Qualified Domain Name associated with the request.
+ final String fullyQualifiedName;
+
+ /// The [QuestionType], i.e. multicast or unicast.
+ final int questionType;
+
+ /// Convenience accessor to determine whether the question type is multicast.
+ bool get isMulticast => questionType == QuestionType.multicast;
+
+ /// Convenience accessor to determine whether the question type is unicast.
+ bool get isUnicast => questionType == QuestionType.unicast;
+
+ /// Encodes this query to the raw wire format.
+ List<int> encode() {
+ return encodeMDnsQuery(
+ fullyQualifiedName,
+ type: resourceRecordType,
+ multicast: isMulticast,
+ );
+ }
+
+ @override
+ int get hashCode => _hashValues(
+ <int>[resourceRecordType, fullyQualifiedName.hashCode, questionType]);
+
+ @override
+ bool operator ==(Object other) {
+ return other is ResourceRecordQuery &&
+ other.resourceRecordType == resourceRecordType &&
+ other.fullyQualifiedName == fullyQualifiedName &&
+ other.questionType == questionType;
+ }
+
+ @override
+ String toString() =>
+ '$runtimeType{$fullyQualifiedName, type: ${ResourceRecordType.toDebugString(resourceRecordType)}, isMulticast: $isMulticast}';
+}
+
+/// Base implementation of DNS resource records (RRs).
+@immutable
+abstract class ResourceRecord {
+ /// Creates a new ResourceRecord.
+ const ResourceRecord(this.resourceRecordType, this.name, this.validUntil);
+
+ /// The FQDN for this record.
+ final String name;
+
+ /// The epoch time at which point this record is valid for in the cache.
+ final int validUntil;
+
+ /// The raw resource record value. See [ResourceRecordType] for supported values.
+ final int resourceRecordType;
+
+ String get _additionalInfo;
+
+ @override
+ String toString() =>
+ '$runtimeType{$name, validUntil: ${DateTime.fromMillisecondsSinceEpoch(validUntil)}, $_additionalInfo}';
+
+ @override
+ bool operator ==(Object other) {
+ return other is ResourceRecord && _equals(other);
+ }
+
+ bool _equals(ResourceRecord other) {
+ return other.name == name &&
+ other.validUntil == validUntil &&
+ other.resourceRecordType == resourceRecordType;
+ }
+
+ @override
+ int get hashCode {
+ return _hashValues(<int>[
+ name.hashCode,
+ validUntil.hashCode,
+ resourceRecordType.hashCode,
+ _hashCode,
+ ]);
+ }
+
+ // Subclasses of this class should use _hashValues to create a hash code
+ // that will then get hashed in with the common values on this class.
+ int get _hashCode;
+
+ /// Low level method for encoding this record into an mDNS packet.
+ ///
+ /// Subclasses should provide the packet format of their encapsulated data
+ /// into a `Uint8List`, which could then be used to write a pakcet to send
+ /// as a response for this record type.
+ Uint8List encodeResponseRecord();
+}
+
+/// A Service Pointer for reverse mapping an IP address (DNS "PTR").
+class PtrResourceRecord extends ResourceRecord {
+ /// Creates a new PtrResourceRecord.
+ const PtrResourceRecord(
+ String name,
+ int validUntil, {
+ required this.domainName,
+ }) : super(ResourceRecordType.serverPointer, name, validUntil);
+
+ /// The FQDN for this record.
+ final String domainName;
+
+ @override
+ String get _additionalInfo => 'domainName: $domainName';
+
+ @override
+ bool _equals(ResourceRecord other) {
+ return other is PtrResourceRecord &&
+ other.domainName == domainName &&
+ super._equals(other);
+ }
+
+ @override
+ int get _hashCode => _combineHash(_seedHashPrime, domainName.hashCode);
+
+ @override
+ Uint8List encodeResponseRecord() {
+ return Uint8List.fromList(utf8.encode(domainName));
+ }
+}
+
+/// An IP Address record for IPv4 (DNS "A") or IPv6 (DNS "AAAA") records.
+class IPAddressResourceRecord extends ResourceRecord {
+ /// Creates a new IPAddressResourceRecord.
+ IPAddressResourceRecord(
+ String name,
+ int validUntil, {
+ required this.address,
+ }) : super(
+ address.type == InternetAddressType.IPv4
+ ? ResourceRecordType.addressIPv4
+ : ResourceRecordType.addressIPv6,
+ name,
+ validUntil);
+
+ /// The [InternetAddress] for this record.
+ final InternetAddress address;
+
+ @override
+ String get _additionalInfo => 'address: $address';
+
+ @override
+ bool _equals(ResourceRecord other) {
+ return other is IPAddressResourceRecord && other.address == address;
+ }
+
+ @override
+ int get _hashCode => _combineHash(_seedHashPrime, address.hashCode);
+
+ @override
+ Uint8List encodeResponseRecord() {
+ return Uint8List.fromList(address.rawAddress);
+ }
+}
+
+/// A Service record, capturing a host target and port (DNS "SRV").
+class SrvResourceRecord extends ResourceRecord {
+ /// Creates a new service record.
+ const SrvResourceRecord(
+ String name,
+ int validUntil, {
+ required this.target,
+ required this.port,
+ required this.priority,
+ required this.weight,
+ }) : super(ResourceRecordType.service, name, validUntil);
+
+ /// The hostname for this record.
+ final String target;
+
+ /// The port for this record.
+ final int port;
+
+ /// The relative priority of this service.
+ final int priority;
+
+ /// The weight (used when multiple services have the same priority).
+ final int weight;
+
+ @override
+ String get _additionalInfo =>
+ 'target: $target, port: $port, priority: $priority, weight: $weight';
+
+ @override
+ bool _equals(ResourceRecord other) {
+ return other is SrvResourceRecord &&
+ other.target == target &&
+ other.port == port &&
+ other.priority == priority &&
+ other.weight == weight;
+ }
+
+ @override
+ int get _hashCode => _hashValues(<int>[
+ target.hashCode,
+ port.hashCode,
+ priority.hashCode,
+ weight.hashCode,
+ ]);
+
+ @override
+ Uint8List encodeResponseRecord() {
+ final List<int> data = utf8.encode(target);
+ final Uint8List result = Uint8List(data.length + 7);
+ final ByteData resultData = ByteData.view(result.buffer);
+ resultData.setUint16(0, priority);
+ resultData.setUint16(2, weight);
+ resultData.setUint16(4, port);
+ result[6] = data.length;
+ return result..setRange(7, data.length, data);
+ }
+}
+
+/// A Text record, contianing additional textual data (DNS "TXT").
+class TxtResourceRecord extends ResourceRecord {
+ /// Creates a new text record.
+ const TxtResourceRecord(
+ String name,
+ int validUntil, {
+ required this.text,
+ }) : super(ResourceRecordType.text, name, validUntil);
+
+ /// The raw text from this record.
+ final String text;
+
+ @override
+ String get _additionalInfo => 'text: $text';
+
+ @override
+ bool _equals(ResourceRecord other) {
+ return other is TxtResourceRecord && other.text == text;
+ }
+
+ @override
+ int get _hashCode => _combineHash(_seedHashPrime, text.hashCode);
+
+ @override
+ Uint8List encodeResponseRecord() {
+ return Uint8List.fromList(utf8.encode(text));
+ }
+}
diff --git a/packages/multicast_dns/pubspec.yaml b/packages/multicast_dns/pubspec.yaml
new file mode 100644
index 0000000..058d0c4
--- /dev/null
+++ b/packages/multicast_dns/pubspec.yaml
@@ -0,0 +1,13 @@
+name: multicast_dns
+description: Dart package for mDNS queries (e.g. Bonjour, Avahi).
+homepage: https://github.com/flutter/packages/tree/master/packages/multicast_dns
+version: 0.3.0
+
+dependencies:
+ meta: ^1.3.0
+
+dev_dependencies:
+ test: "^1.16.5"
+
+environment:
+ sdk: ">=2.12.0-0 <3.0.0"
diff --git a/packages/multicast_dns/test/client_test.dart b/packages/multicast_dns/test/client_test.dart
new file mode 100644
index 0000000..85ab0b1
--- /dev/null
+++ b/packages/multicast_dns/test/client_test.dart
@@ -0,0 +1,47 @@
+// Copyright (c) 2015, the Dartino 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:multicast_dns/multicast_dns.dart';
+import 'package:test/fake.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('Can inject datagram socket factory and configure mdns port', () async {
+ late int lastPort;
+ final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket();
+ final MDnsClient client = MDnsClient(rawDatagramSocketFactory:
+ (dynamic host, int port,
+ {bool reuseAddress = true,
+ bool reusePort = true,
+ int ttl = 1}) async {
+ lastPort = port;
+ return datagramSocket;
+ });
+
+ await client.start(
+ mDnsPort: 1234,
+ interfacesFactory: (InternetAddressType type) async =>
+ <NetworkInterface>[]);
+
+ expect(lastPort, 1234);
+ });
+}
+
+class FakeRawDatagramSocket extends Fake implements RawDatagramSocket {
+ @override
+ InternetAddress get address => InternetAddress.anyIPv4;
+
+ @override
+ StreamSubscription<RawSocketEvent> listen(
+ void Function(RawSocketEvent event)? onData,
+ {Function? onError,
+ void Function()? onDone,
+ bool? cancelOnError}) {
+ return const Stream<RawSocketEvent>.empty().listen(onData,
+ onError: onError, cancelOnError: cancelOnError, onDone: onDone);
+ }
+}
diff --git a/packages/multicast_dns/test/decode_test.dart b/packages/multicast_dns/test/decode_test.dart
new file mode 100644
index 0000000..f6c5e88
--- /dev/null
+++ b/packages/multicast_dns/test/decode_test.dart
@@ -0,0 +1,1601 @@
+// Copyright (c) 2015, the Dartino 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:test/test.dart';
+import 'package:multicast_dns/src/packet.dart';
+import 'package:multicast_dns/src/resource_record.dart';
+
+const int _kSrvHeaderSize = 6;
+
+void main() {
+ testValidPackages();
+ testBadPackages();
+ testNonUtf8DomainName();
+ // testHexDumpList();
+ testPTRRData();
+ testSRVRData();
+}
+
+void testValidPackages() {
+ test('Can decode valid packets', () {
+ List<ResourceRecord> result = decodeMDnsResponse(package1)!;
+ expect(result, isNotNull);
+ expect(result.length, 1);
+ IPAddressResourceRecord ipResult = result[0] as IPAddressResourceRecord;
+ expect(ipResult.name, 'raspberrypi.local');
+ expect(ipResult.address.address, '192.168.1.191');
+
+ result = decodeMDnsResponse(package2)!;
+ expect(result.length, 2);
+ ipResult = result[0] as IPAddressResourceRecord;
+ expect(ipResult.name, 'raspberrypi.local');
+ expect(ipResult.address.address, '192.168.1.191');
+ ipResult = result[1] as IPAddressResourceRecord;
+ expect(ipResult.name, 'raspberrypi.local');
+ expect(ipResult.address.address, '169.254.95.83');
+
+ result = decodeMDnsResponse(package3)!;
+ expect(result.length, 8);
+ expect(result, <ResourceRecord>[
+ TxtResourceRecord(
+ 'raspberrypi [b8:27:eb:03:92:4b]._workstation._tcp.local',
+ result[0].validUntil,
+ text: '',
+ ),
+ PtrResourceRecord(
+ '_udisks-ssh._tcp.local',
+ result[1].validUntil,
+ domainName: 'raspberrypi._udisks-ssh._tcp.local',
+ ),
+ SrvResourceRecord(
+ 'raspberrypi._udisks-ssh._tcp.local',
+ result[2].validUntil,
+ target: 'raspberrypi.local',
+ port: 22,
+ priority: 0,
+ weight: 0,
+ ),
+ TxtResourceRecord(
+ 'raspberrypi._udisks-ssh._tcp.local',
+ result[3].validUntil,
+ text: '',
+ ),
+ PtrResourceRecord('_services._dns-sd._udp.local', result[4].validUntil,
+ domainName: '_udisks-ssh._tcp.local'),
+ PtrResourceRecord(
+ '_workstation._tcp.local',
+ result[5].validUntil,
+ domainName: 'raspberrypi [b8:27:eb:03:92:4b]._workstation._tcp.local',
+ ),
+ SrvResourceRecord(
+ 'raspberrypi [b8:27:eb:03:92:4b]._workstation._tcp.local',
+ result[6].validUntil,
+ target: 'raspberrypi.local',
+ port: 9,
+ priority: 0,
+ weight: 0,
+ ),
+ PtrResourceRecord(
+ '_services._dns-sd._udp.local',
+ result[7].validUntil,
+ domainName: '_workstation._tcp.local',
+ ),
+ ]);
+
+ result = decodeMDnsResponse(packagePtrResponse)!;
+ expect(6, result.length);
+ expect(result, <ResourceRecord>[
+ PtrResourceRecord(
+ '_fletch_agent._tcp.local',
+ result[0].validUntil,
+ domainName: 'fletch-agent on raspberrypi._fletch_agent._tcp.local',
+ ),
+ TxtResourceRecord(
+ 'fletch-agent on raspberrypi._fletch_agent._tcp.local',
+ result[1].validUntil,
+ text: '',
+ ),
+ SrvResourceRecord(
+ 'fletch-agent on raspberrypi._fletch_agent._tcp.local',
+ result[2].validUntil,
+ target: 'raspberrypi.local',
+ port: 12121,
+ priority: 0,
+ weight: 0,
+ ),
+ IPAddressResourceRecord(
+ 'raspberrypi.local',
+ result[3].validUntil,
+ address: InternetAddress('fe80:0000:0000:0000:ba27:ebff:fe69:6e3a'),
+ ),
+ IPAddressResourceRecord(
+ 'raspberrypi.local',
+ result[4].validUntil,
+ address: InternetAddress('192.168.1.1'),
+ ),
+ IPAddressResourceRecord(
+ 'raspberrypi.local',
+ result[5].validUntil,
+ address: InternetAddress('169.254.167.172'),
+ ),
+ ]);
+ });
+
+ // Fixes https://github.com/flutter/flutter/issues/31854
+ test('Can decode packages with question, answer and additional', () {
+ final List<ResourceRecord> result =
+ decodeMDnsResponse(packetWithQuestionAnArCount)!;
+ expect(result, isNotNull);
+ expect(result.length, 2);
+ expect(result, <ResourceRecord>[
+ PtrResourceRecord(
+ '_______________.____._____',
+ result[0].validUntil,
+ domainName: '_______________________._______________.____._____',
+ ),
+ PtrResourceRecord(
+ '_______________.____._____',
+ result[1].validUntil,
+ domainName: '____________________________._______________.____._____',
+ ),
+ ]);
+ });
+
+ // Fixes https://github.com/flutter/flutter/issues/31854
+ test('Can decode packages without question and with answer and additional',
+ () {
+ final List<ResourceRecord> result =
+ decodeMDnsResponse(packetWithoutQuestionWithAnArCount)!;
+ expect(result, isNotNull);
+ expect(result.length, 2);
+ expect(result, <ResourceRecord>[
+ PtrResourceRecord(
+ '_______________.____._____',
+ result[0].validUntil,
+ domainName: '______________________._______________.____._____',
+ ),
+ TxtResourceRecord(
+ '_______________.____._____',
+ result[1].validUntil,
+ text: 'model=MacBookPro14,3\nosxvers=18\necolor=225,225,223\n',
+ ),
+ ]);
+ });
+
+ test('Can decode packages with a long text resource', () {
+ final List<ResourceRecord> result = decodeMDnsResponse(packetWithLongTxt)!;
+ expect(result, isNotNull);
+ expect(result.length, 2);
+ expect(result, <ResourceRecord>[
+ PtrResourceRecord(
+ '_______________.____._____',
+ result[0].validUntil,
+ domainName: '______________________._______________.____._____',
+ ),
+ TxtResourceRecord(
+ '_______________.____._____',
+ result[1].validUntil,
+ text: (')' * 129) + '\n',
+ ),
+ ]);
+ });
+}
+
+void testBadPackages() {
+ test('Returns null for invalid packets', () {
+ for (final List<int> p in <List<int>>[package1, package2, package3]) {
+ for (int i = 0; i < p.length; i++) {
+ expect(decodeMDnsResponse(p.sublist(0, i)), isNull);
+ }
+ }
+ });
+}
+
+void testPTRRData() {
+ test('Can read FQDN from PTR data', () {
+ expect('sgjesse-macbookpro2 [78:31:c1:b8:55:38]._workstation._tcp.local',
+ readFQDN(ptrRData));
+ expect('fletch-agent._fletch_agent._tcp.local', readFQDN(ptrRData2));
+ });
+}
+
+void testSRVRData() {
+ test('Can read FQDN from SRV data', () {
+ expect('fletch.local', readFQDN(srvRData, _kSrvHeaderSize));
+ });
+}
+
+void testNonUtf8DomainName() {
+ test('Returns non-null for non-utf8 domain name', () {
+ final List<ResourceRecord> result = decodeMDnsResponse(nonUtf8Package)!;
+ expect(result, isNotNull);
+ expect(result[0] is TxtResourceRecord, isTrue);
+ final TxtResourceRecord txt = result[0] as TxtResourceRecord;
+ expect(txt.name, contains('�'));
+ });
+}
+
+// One address.
+const List<int> package1 = <int>[
+ 0x00,
+ 0x00,
+ 0x84,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x0b,
+ 0x72,
+ 0x61,
+ 0x73,
+ 0x70,
+ 0x62,
+ 0x65,
+ 0x72,
+ 0x72,
+ 0x79,
+ 0x70,
+ 0x69,
+ 0x05,
+ 0x6c,
+ 0x6f,
+ 0x63,
+ 0x61,
+ 0x6c,
+ 0x00,
+ 0x00,
+ 0x01,
+ 0x80,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x78,
+ 0x00,
+ 0x04,
+ 0xc0,
+ 0xa8,
+ 0x01,
+ 0xbf
+];
+
+// Two addresses.
+const List<int> package2 = <int>[
+ 0x00,
+ 0x00,
+ 0x84,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x02,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x0b,
+ 0x72,
+ 0x61,
+ 0x73,
+ 0x70,
+ 0x62,
+ 0x65,
+ 0x72,
+ 0x72,
+ 0x79,
+ 0x70,
+ 0x69,
+ 0x05,
+ 0x6c,
+ 0x6f,
+ 0x63,
+ 0x61,
+ 0x6c,
+ 0x00,
+ 0x00,
+ 0x01,
+ 0x80,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x78,
+ 0x00,
+ 0x04,
+ 0xc0,
+ 0xa8,
+ 0x01,
+ 0xbf,
+ 0xc0,
+ 0x0c,
+ 0x00,
+ 0x01,
+ 0x80,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x78,
+ 0x00,
+ 0x04,
+ 0xa9,
+ 0xfe,
+ 0x5f,
+ 0x53
+];
+
+// Eight mixed answers.
+const List<int> package3 = <int>[
+ 0x00,
+ 0x00,
+ 0x84,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x08,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x1f,
+ 0x72,
+ 0x61,
+ 0x73,
+ 0x70,
+ 0x62,
+ 0x65,
+ 0x72,
+ 0x72,
+ 0x79,
+ 0x70,
+ 0x69,
+ 0x20,
+ 0x5b,
+ 0x62,
+ 0x38,
+ 0x3a,
+ 0x32,
+ 0x37,
+ 0x3a,
+ 0x65,
+ 0x62,
+ 0x3a,
+ 0x30,
+ 0x33,
+ 0x3a,
+ 0x39,
+ 0x32,
+ 0x3a,
+ 0x34,
+ 0x62,
+ 0x5d,
+ 0x0c,
+ 0x5f,
+ 0x77,
+ 0x6f,
+ 0x72,
+ 0x6b,
+ 0x73,
+ 0x74,
+ 0x61,
+ 0x74,
+ 0x69,
+ 0x6f,
+ 0x6e,
+ 0x04,
+ 0x5f,
+ 0x74,
+ 0x63,
+ 0x70,
+ 0x05,
+ 0x6c,
+ 0x6f,
+ 0x63,
+ 0x61,
+ 0x6c,
+ 0x00,
+ 0x00,
+ 0x10,
+ 0x80,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x11,
+ 0x94,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x0b,
+ 0x5f,
+ 0x75,
+ 0x64,
+ 0x69,
+ 0x73,
+ 0x6b,
+ 0x73,
+ 0x2d,
+ 0x73,
+ 0x73,
+ 0x68,
+ 0xc0,
+ 0x39,
+ 0x00,
+ 0x0c,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x11,
+ 0x94,
+ 0x00,
+ 0x0e,
+ 0x0b,
+ 0x72,
+ 0x61,
+ 0x73,
+ 0x70,
+ 0x62,
+ 0x65,
+ 0x72,
+ 0x72,
+ 0x79,
+ 0x70,
+ 0x69,
+ 0xc0,
+ 0x50,
+ 0xc0,
+ 0x68,
+ 0x00,
+ 0x21,
+ 0x80,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x78,
+ 0x00,
+ 0x14,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x16,
+ 0x0b,
+ 0x72,
+ 0x61,
+ 0x73,
+ 0x70,
+ 0x62,
+ 0x65,
+ 0x72,
+ 0x72,
+ 0x79,
+ 0x70,
+ 0x69,
+ 0xc0,
+ 0x3e,
+ 0xc0,
+ 0x68,
+ 0x00,
+ 0x10,
+ 0x80,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x11,
+ 0x94,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x09,
+ 0x5f,
+ 0x73,
+ 0x65,
+ 0x72,
+ 0x76,
+ 0x69,
+ 0x63,
+ 0x65,
+ 0x73,
+ 0x07,
+ 0x5f,
+ 0x64,
+ 0x6e,
+ 0x73,
+ 0x2d,
+ 0x73,
+ 0x64,
+ 0x04,
+ 0x5f,
+ 0x75,
+ 0x64,
+ 0x70,
+ 0xc0,
+ 0x3e,
+ 0x00,
+ 0x0c,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x11,
+ 0x94,
+ 0x00,
+ 0x02,
+ 0xc0,
+ 0x50,
+ 0xc0,
+ 0x2c,
+ 0x00,
+ 0x0c,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x11,
+ 0x94,
+ 0x00,
+ 0x02,
+ 0xc0,
+ 0x0c,
+ 0xc0,
+ 0x0c,
+ 0x00,
+ 0x21,
+ 0x80,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x78,
+ 0x00,
+ 0x08,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x09,
+ 0xc0,
+ 0x88,
+ 0xc0,
+ 0xa3,
+ 0x00,
+ 0x0c,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x11,
+ 0x94,
+ 0x00,
+ 0x02,
+ 0xc0,
+ 0x2c
+];
+
+const List<int> packagePtrResponse = <int>[
+ 0x00,
+ 0x00,
+ 0x84,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x06,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x0d,
+ 0x5f,
+ 0x66,
+ 0x6c,
+ 0x65,
+ 0x74,
+ 0x63,
+ 0x68,
+ 0x5f,
+ 0x61,
+ 0x67,
+ 0x65,
+ 0x6e,
+ 0x74,
+ 0x04,
+ 0x5f,
+ 0x74,
+ 0x63,
+ 0x70,
+ 0x05,
+ 0x6c,
+ 0x6f,
+ 0x63,
+ 0x61,
+ 0x6c,
+ 0x00,
+ 0x00,
+ 0x0c,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x11,
+ 0x94,
+ 0x00,
+ 0x1e,
+ 0x1b,
+ 0x66,
+ 0x6c,
+ 0x65,
+ 0x74,
+ 0x63,
+ 0x68,
+ 0x2d,
+ 0x61,
+ 0x67,
+ 0x65,
+ 0x6e,
+ 0x74,
+ 0x20,
+ 0x6f,
+ 0x6e,
+ 0x20,
+ 0x72,
+ 0x61,
+ 0x73,
+ 0x70,
+ 0x62,
+ 0x65,
+ 0x72,
+ 0x72,
+ 0x79,
+ 0x70,
+ 0x69,
+ 0xc0,
+ 0x0c,
+ 0xc0,
+ 0x30,
+ 0x00,
+ 0x10,
+ 0x80,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x11,
+ 0x94,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0xc0,
+ 0x30,
+ 0x00,
+ 0x21,
+ 0x80,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x78,
+ 0x00,
+ 0x14,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x2f,
+ 0x59,
+ 0x0b,
+ 0x72,
+ 0x61,
+ 0x73,
+ 0x70,
+ 0x62,
+ 0x65,
+ 0x72,
+ 0x72,
+ 0x79,
+ 0x70,
+ 0x69,
+ 0xc0,
+ 0x1f,
+ 0xc0,
+ 0x6d,
+ 0x00,
+ 0x1c,
+ 0x80,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x78,
+ 0x00,
+ 0x10,
+ 0xfe,
+ 0x80,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0xba,
+ 0x27,
+ 0xeb,
+ 0xff,
+ 0xfe,
+ 0x69,
+ 0x6e,
+ 0x3a,
+ 0xc0,
+ 0x6d,
+ 0x00,
+ 0x01,
+ 0x80,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x78,
+ 0x00,
+ 0x04,
+ 0xc0,
+ 0xa8,
+ 0x01,
+ 0x01,
+ 0xc0,
+ 0x6d,
+ 0x00,
+ 0x01,
+ 0x80,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x78,
+ 0x00,
+ 0x04,
+ 0xa9,
+ 0xfe,
+ 0xa7,
+ 0xac
+];
+
+const List<int> ptrRData = <int>[
+ 0x27,
+ 0x73,
+ 0x67,
+ 0x6a,
+ 0x65,
+ 0x73,
+ 0x73,
+ 0x65,
+ 0x2d,
+ 0x6d,
+ 0x61,
+ 0x63,
+ 0x62,
+ 0x6f,
+ 0x6f,
+ 0x6b,
+ 0x70,
+ 0x72,
+ 0x6f,
+ 0x32,
+ 0x20,
+ 0x5b,
+ 0x37,
+ 0x38,
+ 0x3a,
+ 0x33,
+ 0x31,
+ 0x3a,
+ 0x63,
+ 0x31,
+ 0x3a,
+ 0x62,
+ 0x38,
+ 0x3a,
+ 0x35,
+ 0x35,
+ 0x3a,
+ 0x33,
+ 0x38,
+ 0x5d,
+ 0x0c,
+ 0x5f,
+ 0x77,
+ 0x6f,
+ 0x72,
+ 0x6b,
+ 0x73,
+ 0x74,
+ 0x61,
+ 0x74,
+ 0x69,
+ 0x6f,
+ 0x6e,
+ 0x04,
+ 0x5f,
+ 0x74,
+ 0x63,
+ 0x70,
+ 0x05,
+ 0x6c,
+ 0x6f,
+ 0x63,
+ 0x61,
+ 0x6c,
+ 0x00
+];
+
+const List<int> ptrRData2 = <int>[
+ 0x0c,
+ 0x66,
+ 0x6c,
+ 0x65,
+ 0x74,
+ 0x63,
+ 0x68,
+ 0x2d,
+ 0x61,
+ 0x67,
+ 0x65,
+ 0x6e,
+ 0x74,
+ 0x0d,
+ 0x5f,
+ 0x66,
+ 0x6c,
+ 0x65,
+ 0x74,
+ 0x63,
+ 0x68,
+ 0x5f,
+ 0x61,
+ 0x67,
+ 0x65,
+ 0x6e,
+ 0x74,
+ 0x04,
+ 0x5f,
+ 0x74,
+ 0x63,
+ 0x70,
+ 0x05,
+ 0x6c,
+ 0x6f,
+ 0x63,
+ 0x61,
+ 0x6c,
+ 0x00
+];
+
+const List<int> srvRData = <int>[
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x2f,
+ 0x59,
+ 0x06,
+ 0x66,
+ 0x6c,
+ 0x65,
+ 0x74,
+ 0x63,
+ 0x68,
+ 0x05,
+ 0x6c,
+ 0x6f,
+ 0x63,
+ 0x61,
+ 0x6c,
+ 0x00
+];
+
+const List<int> packetWithQuestionAnArCount = <int>[
+ 0,
+ 0,
+ 2,
+ 0,
+ 0,
+ 1,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 1,
+ 15,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 4,
+ 95,
+ 95,
+ 95,
+ 95,
+ 5,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 0,
+ 0,
+ 12,
+ 0,
+ 1,
+ 192,
+ 12,
+ 0,
+ 12,
+ 0,
+ 1,
+ 0,
+ 0,
+ 14,
+ 13,
+ 0,
+ 26,
+ 23,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 192,
+ 12,
+ 192,
+ 12,
+ 0,
+ 12,
+ 0,
+ 1,
+ 0,
+ 0,
+ 14,
+ 13,
+ 0,
+ 31,
+ 28,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 192,
+ 12,
+];
+
+const List<int> packetWithoutQuestionWithAnArCount = <int>[
+ 0,
+ 0,
+ 132,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 1,
+ 15,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 4,
+ 95,
+ 95,
+ 95,
+ 95,
+ 5,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 0,
+ 0,
+ 12,
+ 0,
+ 1,
+ 0,
+ 0,
+ 17,
+ 148,
+ 0,
+ 25,
+ 22,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 192,
+ 12,
+ 22,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 12,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 192,
+ 28,
+ 0,
+ 16,
+ 0,
+ 1,
+ 0,
+ 0,
+ 17,
+ 148,
+ 0,
+ 51,
+ 20,
+ 109,
+ 111,
+ 100,
+ 101,
+ 108,
+ 61,
+ 77,
+ 97,
+ 99,
+ 66,
+ 111,
+ 111,
+ 107,
+ 80,
+ 114,
+ 111,
+ 49,
+ 52,
+ 44,
+ 51,
+ 10,
+ 111,
+ 115,
+ 120,
+ 118,
+ 101,
+ 114,
+ 115,
+ 61,
+ 49,
+ 56,
+ 18,
+ 101,
+ 99,
+ 111,
+ 108,
+ 111,
+ 114,
+ 61,
+ 50,
+ 50,
+ 53,
+ 44,
+ 50,
+ 50,
+ 53,
+ 44,
+ 50,
+ 50,
+ 51,
+];
+
+// This is the same as packetWithoutQuestionWithAnArCount, but the text
+// resource just has a single long string. If the length isn't decoded
+// separately from the string, there will be utf8 decoding failures.
+const List<int> packetWithLongTxt = <int>[
+ 0,
+ 0,
+ 132,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 1,
+ 15,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 4,
+ 95,
+ 95,
+ 95,
+ 95,
+ 5,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 0,
+ 0,
+ 12,
+ 0,
+ 1,
+ 0,
+ 0,
+ 17,
+ 148,
+ 0,
+ 25,
+ 22,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 192,
+ 12,
+ 22,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 12,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 95,
+ 192,
+ 28,
+ 0,
+ 16,
+ 0,
+ 1,
+ 0,
+ 0,
+ 17,
+ 148,
+ 0,
+ 51,
+ // Long string starts here.
+ 129,
+ 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // 16
+ 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // 32
+ 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, //
+ 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // 64
+ 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, //
+ 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, //
+ 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, //
+ 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // 128,
+ 41, // 129
+];
+
+// Package with a domain name that is not valid utf-8.
+const List<int> nonUtf8Package = <int>[
+ 0x00,
+ 0x00,
+ 0x84,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x08,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x1f,
+ 0x72,
+ 0x61,
+ 0x73,
+ 0x70,
+ 0x62,
+ 0x65,
+ 0x72,
+ 0x72,
+ 0x79,
+ 0x70,
+ 0x69,
+ 0x20,
+ 0x5b,
+ 0x62,
+ 0x38,
+ 0x3a,
+ 0x32,
+ 0x37,
+ 0x3a,
+ 0x65,
+ 0x62,
+ 0xd2,
+ 0x30,
+ 0x33,
+ 0x3a,
+ 0x39,
+ 0x32,
+ 0x3a,
+ 0x34,
+ 0x62,
+ 0x5d,
+ 0x0c,
+ 0x5f,
+ 0x77,
+ 0x6f,
+ 0x72,
+ 0x6b,
+ 0x73,
+ 0x74,
+ 0x61,
+ 0x74,
+ 0x69,
+ 0x6f,
+ 0x6e,
+ 0x04,
+ 0x5f,
+ 0x74,
+ 0x63,
+ 0x70,
+ 0x05,
+ 0x6c,
+ 0x6f,
+ 0x63,
+ 0x61,
+ 0x6c,
+ 0x00,
+ 0x00,
+ 0x10,
+ 0x80,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x11,
+ 0x94,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x0b,
+ 0x5f,
+ 0x75,
+ 0x64,
+ 0x69,
+ 0x73,
+ 0x6b,
+ 0x73,
+ 0x2d,
+ 0x73,
+ 0x73,
+ 0x68,
+ 0xc0,
+ 0x39,
+ 0x00,
+ 0x0c,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x11,
+ 0x94,
+ 0x00,
+ 0x0e,
+ 0x0b,
+ 0x72,
+ 0x61,
+ 0x73,
+ 0x70,
+ 0x62,
+ 0x65,
+ 0x72,
+ 0x72,
+ 0x79,
+ 0x70,
+ 0x69,
+ 0xc0,
+ 0x50,
+ 0xc0,
+ 0x68,
+ 0x00,
+ 0x21,
+ 0x80,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x78,
+ 0x00,
+ 0x14,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x16,
+ 0x0b,
+ 0x72,
+ 0x61,
+ 0x73,
+ 0x70,
+ 0x62,
+ 0x65,
+ 0x72,
+ 0x72,
+ 0x79,
+ 0x70,
+ 0x69,
+ 0xc0,
+ 0x3e,
+ 0xc0,
+ 0x68,
+ 0x00,
+ 0x10,
+ 0x80,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x11,
+ 0x94,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x09,
+ 0x5f,
+ 0x73,
+ 0x65,
+ 0x72,
+ 0x76,
+ 0x69,
+ 0x63,
+ 0x65,
+ 0x73,
+ 0x07,
+ 0x5f,
+ 0x64,
+ 0x6e,
+ 0x73,
+ 0x2d,
+ 0x73,
+ 0x64,
+ 0x04,
+ 0x5f,
+ 0x75,
+ 0x64,
+ 0x70,
+ 0xc0,
+ 0x3e,
+ 0x00,
+ 0x0c,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x11,
+ 0x94,
+ 0x00,
+ 0x02,
+ 0xc0,
+ 0x50,
+ 0xc0,
+ 0x2c,
+ 0x00,
+ 0x0c,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x11,
+ 0x94,
+ 0x00,
+ 0x02,
+ 0xc0,
+ 0x0c,
+ 0xc0,
+ 0x0c,
+ 0x00,
+ 0x21,
+ 0x80,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x78,
+ 0x00,
+ 0x08,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x09,
+ 0xc0,
+ 0x88,
+ 0xc0,
+ 0xa3,
+ 0x00,
+ 0x0c,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x11,
+ 0x94,
+ 0x00,
+ 0x02,
+ 0xc0,
+ 0x2c
+];
diff --git a/packages/multicast_dns/test/lookup_resolver_test.dart b/packages/multicast_dns/test/lookup_resolver_test.dart
new file mode 100644
index 0000000..3423a68
--- /dev/null
+++ b/packages/multicast_dns/test/lookup_resolver_test.dart
@@ -0,0 +1,102 @@
+// Copyright (c) 2015, the Dartino 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:test/test.dart';
+import 'package:multicast_dns/src/lookup_resolver.dart';
+import 'package:multicast_dns/src/resource_record.dart';
+
+void main() {
+ testTimeout();
+ testResult();
+ testResult2();
+ testResult3();
+}
+
+ResourceRecord ip4Result(String name, InternetAddress address) {
+ final int validUntil = DateTime.now().millisecondsSinceEpoch + 2000;
+ return IPAddressResourceRecord(name, validUntil, address: address);
+}
+
+void testTimeout() {
+ test('Resolver does not return with short timeout', () async {
+ const Duration shortTimeout = Duration(milliseconds: 1);
+ final LookupResolver resolver = LookupResolver();
+ final Stream<ResourceRecord> result = resolver.addPendingRequest(
+ ResourceRecordType.addressIPv4, 'xxx', shortTimeout);
+ expect(await result.isEmpty, isTrue);
+ });
+}
+
+// One pending request and one response.
+void testResult() {
+ test('One pending request and one response', () async {
+ const Duration noTimeout = Duration(days: 1);
+ final LookupResolver resolver = LookupResolver();
+ final Stream<ResourceRecord> futureResult = resolver.addPendingRequest(
+ ResourceRecordType.addressIPv4, 'xxx.local', noTimeout);
+ final ResourceRecord response =
+ ip4Result('xxx.local', InternetAddress('1.2.3.4'));
+ resolver.handleResponse(<ResourceRecord>[response]);
+ final IPAddressResourceRecord result =
+ await futureResult.first as IPAddressResourceRecord;
+ expect('1.2.3.4', result.address.address);
+ resolver.clearPendingRequests();
+ });
+}
+
+void testResult2() {
+ test('Two requests', () async {
+ const Duration noTimeout = Duration(days: 1);
+ final LookupResolver resolver = LookupResolver();
+ final Stream<ResourceRecord> futureResult1 = resolver.addPendingRequest(
+ ResourceRecordType.addressIPv4, 'xxx.local', noTimeout);
+ final Stream<ResourceRecord> futureResult2 = resolver.addPendingRequest(
+ ResourceRecordType.addressIPv4, 'yyy.local', noTimeout);
+ final ResourceRecord response1 =
+ ip4Result('xxx.local', InternetAddress('1.2.3.4'));
+ final ResourceRecord response2 =
+ ip4Result('yyy.local', InternetAddress('2.3.4.5'));
+ resolver.handleResponse(<ResourceRecord>[response2, response1]);
+ final IPAddressResourceRecord result1 =
+ await futureResult1.first as IPAddressResourceRecord;
+ final IPAddressResourceRecord result2 =
+ await futureResult2.first as IPAddressResourceRecord;
+ expect('1.2.3.4', result1.address.address);
+ expect('2.3.4.5', result2.address.address);
+ resolver.clearPendingRequests();
+ });
+}
+
+void testResult3() {
+ test('Multiple requests', () async {
+ const Duration noTimeout = Duration(days: 1);
+ final LookupResolver resolver = LookupResolver();
+ final ResourceRecord response0 =
+ ip4Result('zzz.local', InternetAddress('2.3.4.5'));
+ resolver.handleResponse(<ResourceRecord>[response0]);
+ final Stream<ResourceRecord> futureResult1 = resolver.addPendingRequest(
+ ResourceRecordType.addressIPv4, 'xxx.local', noTimeout);
+ resolver.handleResponse(<ResourceRecord>[response0]);
+ final Stream<ResourceRecord> futureResult2 = resolver.addPendingRequest(
+ ResourceRecordType.addressIPv4, 'yyy.local', noTimeout);
+ resolver.handleResponse(<ResourceRecord>[response0]);
+ final ResourceRecord response1 =
+ ip4Result('xxx.local', InternetAddress('1.2.3.4'));
+ resolver.handleResponse(<ResourceRecord>[response0]);
+ final ResourceRecord response2 =
+ ip4Result('yyy.local', InternetAddress('2.3.4.5'));
+ resolver.handleResponse(<ResourceRecord>[response0]);
+ resolver.handleResponse(<ResourceRecord>[response2, response1]);
+ resolver.handleResponse(<ResourceRecord>[response0]);
+ final IPAddressResourceRecord result1 =
+ await futureResult1.first as IPAddressResourceRecord;
+ final IPAddressResourceRecord result2 =
+ await futureResult2.first as IPAddressResourceRecord;
+ expect('1.2.3.4', result1.address.address);
+ expect('2.3.4.5', result2.address.address);
+ resolver.clearPendingRequests();
+ });
+}
diff --git a/packages/multicast_dns/test/resource_record_cache_test.dart b/packages/multicast_dns/test/resource_record_cache_test.dart
new file mode 100644
index 0000000..1b9130f
--- /dev/null
+++ b/packages/multicast_dns/test/resource_record_cache_test.dart
@@ -0,0 +1,84 @@
+// Copyright (c) 2015, the Dartino 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.
+
+// Test that the resource record cache works correctly. In particular, make
+// sure that it removes all entries for a name before insertingrecords
+// of that name.
+
+import 'dart:io';
+
+import 'package:test/test.dart';
+import 'package:multicast_dns/src/resource_record.dart';
+import 'package:multicast_dns/src/native_protocol_client.dart'
+ show ResourceRecordCache;
+
+void main() {
+ testOverwrite();
+ testTimeout();
+}
+
+void testOverwrite() {
+ test('Cache can overwrite entries', () {
+ final InternetAddress ip1 = InternetAddress('192.168.1.1');
+ final InternetAddress ip2 = InternetAddress('192.168.1.2');
+ final int valid = DateTime.now().millisecondsSinceEpoch + 86400 * 1000;
+
+ final ResourceRecordCache cache = ResourceRecordCache();
+
+ // Add two different records.
+ cache.updateRecords(<ResourceRecord>[
+ IPAddressResourceRecord('hest', valid, address: ip1),
+ IPAddressResourceRecord('fisk', valid, address: ip2)
+ ]);
+ expect(cache.entryCount, 2);
+
+ // Update these records.
+ cache.updateRecords(<ResourceRecord>[
+ IPAddressResourceRecord('hest', valid, address: ip1),
+ IPAddressResourceRecord('fisk', valid, address: ip2)
+ ]);
+ expect(cache.entryCount, 2);
+
+ // Add two records with the same name (should remove the old one
+ // with that name only.)
+ cache.updateRecords(<ResourceRecord>[
+ IPAddressResourceRecord('hest', valid, address: ip1),
+ IPAddressResourceRecord('hest', valid, address: ip2)
+ ]);
+ expect(cache.entryCount, 3);
+
+ // Overwrite the two cached entries with one with the same name.
+ cache.updateRecords(<ResourceRecord>[
+ IPAddressResourceRecord('hest', valid, address: ip1),
+ ]);
+ expect(cache.entryCount, 2);
+ });
+}
+
+void testTimeout() {
+ test('Cache can evict records after timeout', () {
+ final InternetAddress ip1 = InternetAddress('192.168.1.1');
+ final int valid = DateTime.now().millisecondsSinceEpoch + 86400 * 1000;
+ final int notValid = DateTime.now().millisecondsSinceEpoch - 1;
+
+ final ResourceRecordCache cache = ResourceRecordCache();
+
+ cache.updateRecords(
+ <ResourceRecord>[IPAddressResourceRecord('hest', valid, address: ip1)]);
+ expect(cache.entryCount, 1);
+
+ cache.updateRecords(<ResourceRecord>[
+ IPAddressResourceRecord('fisk', notValid, address: ip1)
+ ]);
+
+ List<ResourceRecord> results = <ResourceRecord>[];
+ cache.lookup('hest', ResourceRecordType.addressIPv4, results);
+ expect(results.isEmpty, isFalse);
+
+ results = <ResourceRecord>[];
+ cache.lookup('fisk', ResourceRecordType.addressIPv4, results);
+ expect(results.isEmpty, isTrue);
+ expect(cache.entryCount, 1);
+ });
+}
diff --git a/packages/multicast_dns/tool/packet_gen.dart b/packages/multicast_dns/tool/packet_gen.dart
new file mode 100644
index 0000000..36486be
--- /dev/null
+++ b/packages/multicast_dns/tool/packet_gen.dart
@@ -0,0 +1,70 @@
+// Copyright (c) 2015, the Dartino 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.
+
+// Support code to generate the hex-lists in test/decode_test.dart from
+// a hex-stream.
+import 'dart:io';
+
+void formatHexStream(String hexStream) {
+ String s = '';
+ for (int i = 0; i < hexStream.length / 2; i++) {
+ if (s.isNotEmpty) {
+ s += ', ';
+ }
+ s += '0x';
+ final String x = hexStream.substring(i * 2, i * 2 + 2);
+ s += x;
+ if (((i + 1) % 8) == 0) {
+ s += ',';
+ print(s);
+ s = '';
+ }
+ }
+ if (s.isNotEmpty) {
+ print(s);
+ }
+}
+
+// Support code for generating the hex-lists in test/decode_test.dart.
+void hexDumpList(List<int> package) {
+ String s = '';
+ for (int i = 0; i < package.length; i++) {
+ if (s.isNotEmpty) {
+ s += ', ';
+ }
+ s += '0x';
+ final String x = package[i].toRadixString(16);
+ if (x.length == 1) {
+ s += '0';
+ }
+ s += x;
+ if (((i + 1) % 8) == 0) {
+ s += ',';
+ print(s);
+ s = '';
+ }
+ }
+ if (s.isNotEmpty) {
+ print(s);
+ }
+}
+
+void dumpDatagram(Datagram datagram) {
+ String _toHex(List<int> ints) {
+ final StringBuffer buffer = StringBuffer();
+ for (int i = 0; i < ints.length; i++) {
+ buffer.write(ints[i].toRadixString(16).padLeft(2, '0'));
+ if ((i + 1) % 10 == 0) {
+ buffer.writeln();
+ } else {
+ buffer.write(' ');
+ }
+ }
+ return buffer.toString();
+ }
+
+ print('${datagram.address.address}:${datagram.port}:');
+ print(_toHex(datagram.data));
+ print('');
+}
diff --git a/packages/palette_generator/CHANGELOG.md b/packages/palette_generator/CHANGELOG.md
new file mode 100644
index 0000000..786dcc7
--- /dev/null
+++ b/packages/palette_generator/CHANGELOG.md
@@ -0,0 +1,35 @@
+## 0.3.0
+
+* Migrated to null safety.
+
+## 0.2.4+1
+
+* Removed a `dart:async` import that isn't required for \>=Dart 2.1.
+
+## 0.2.4
+
+* Fix PaletteGenerator.fromImageProvider region not scaled to fit image size.
+
+## 0.2.3
+
+* Bumped minimum Flutter version to 1.15.21 to pick up Diagnosticable as a mixin and remove DiagnosticableMixin.
+
+## 0.2.2
+
+* Bumped minimum Flutter version to 1.6.7 to pick up DiagnosticableMixin.
+
+## 0.2.1
+
+* PaletteGenerator: performance improvements.
+
+## 0.2.0
+
+* Updated code to be compatible with Flutter breaking change.
+
+## 0.1.1
+
+* Fixed a problem with a listener that wasn't being unregistered properly.
+
+## 0.1.0
+
+* Initial Open Source release.
diff --git a/packages/palette_generator/LICENSE b/packages/palette_generator/LICENSE
new file mode 100644
index 0000000..8211a02
--- /dev/null
+++ b/packages/palette_generator/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014 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/palette_generator/README.md b/packages/palette_generator/README.md
new file mode 100644
index 0000000..3d37881
--- /dev/null
+++ b/packages/palette_generator/README.md
@@ -0,0 +1,22 @@
+# Palette Generator package
+
+[](https://pub.dartlang.org/packages/palette_generator)
+
+A Flutter package to extract prominent colors from an Image, typically used to
+find colors for a user interface.
+
+## Usage
+
+To use this package, add `palette_generator` as a
+[dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/).
+
+## Example
+
+Import the library via
+
+```dart
+import 'package:palette_generator/palette_generator.dart';
+```
+
+Then use the `PaletteGenerator` Dart class in your code. To see how this is done,
+check out the [image_colors example app](example/).
diff --git a/packages/palette_generator/example/.gitignore b/packages/palette_generator/example/.gitignore
new file mode 100644
index 0000000..ae1f183
--- /dev/null
+++ b/packages/palette_generator/example/.gitignore
@@ -0,0 +1,37 @@
+# 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
+
+# Exceptions to above rules.
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/palette_generator/example/.metadata b/packages/palette_generator/example/.metadata
new file mode 100644
index 0000000..96bf554
--- /dev/null
+++ b/packages/palette_generator/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: bb23a110e0e4c2b378cec38fcd5d9b7bef10ec63
+ channel: unknown
diff --git a/packages/palette_generator/example/README.md b/packages/palette_generator/example/README.md
new file mode 100644
index 0000000..cfba2a8
--- /dev/null
+++ b/packages/palette_generator/example/README.md
@@ -0,0 +1,6 @@
+# image_colors
+
+A sample app for demonstrating the PaletteGenerator
+
+This app will show you what kinds of palettes the generator creates, and one
+way to create them from existing image providers.
\ No newline at end of file
diff --git a/packages/palette_generator/example/android/.gitignore b/packages/palette_generator/example/android/.gitignore
new file mode 100644
index 0000000..65b7315
--- /dev/null
+++ b/packages/palette_generator/example/android/.gitignore
@@ -0,0 +1,10 @@
+*.iml
+*.class
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+GeneratedPluginRegistrant.java
diff --git a/packages/palette_generator/example/android/app/build.gradle b/packages/palette_generator/example/android/app/build.gradle
new file mode 100644
index 0000000..640a8a2
--- /dev/null
+++ b/packages/palette_generator/example/android/app/build.gradle
@@ -0,0 +1,56 @@
+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 flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ throw new GradleException("versionName not found. Define flutter.versionName in the local.properties file.")
+}
+
+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.packages.palettegenerator.imagecolors"
+ minSdkVersion 16
+ targetSdkVersion 27
+ versionCode 1
+ 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.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
+}
diff --git a/packages/palette_generator/example/android/app/src/debug/AndroidManifest.xml b/packages/palette_generator/example/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..7b31ebf
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="io.flutter.packages.palettegenerator.imagecolors">
+ <!-- Flutter needs it to communicate with the running application
+ to allow setting breakpoints, to provide hot reload, etc.
+ -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
diff --git a/packages/palette_generator/example/android/app/src/main/AndroidManifest.xml b/packages/palette_generator/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a3cd38c
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="io.flutter.packages.palettegenerator.imagecolors">
+
+ <!-- The INTERNET permission is required for development. Specifically,
+ flutter needs it to communicate with the running application
+ to allow setting breakpoints, to provide hot reload, etc.
+ -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+
+ <!-- io.flutter.app.FlutterApplication is an android.app.Application that
+ calls FlutterMain.startInitialization(this); in its onCreate method.
+ In most cases you can leave this as-is, but you if you want to provide
+ additional functionality it is fine to subclass or reimplement
+ FlutterApplication and put your custom class here. -->
+ <application
+ android:name="io.flutter.app.FlutterApplication"
+ android:label="image_colors"
+ android:icon="@mipmap/ic_launcher">
+ <activity
+ android:name=".MainActivity"
+ android:launchMode="singleTop"
+ android:theme="@style/LaunchTheme"
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
+ android:hardwareAccelerated="true"
+ android:windowSoftInputMode="adjustResize">
+ <!-- This keeps the window background of the activity showing
+ until Flutter renders its first frame. It can be removed if
+ there is no splash screen (such as the default splash screen
+ defined in @style/LaunchTheme). -->
+ <meta-data
+ android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
+ android:value="true" />
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/packages/palette_generator/example/android/app/src/main/java/io/flutter/packages/palettegenerator/imagecolors/MainActivity.java b/packages/palette_generator/example/android/app/src/main/java/io/flutter/packages/palettegenerator/imagecolors/MainActivity.java
new file mode 100644
index 0000000..2ce2e49
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/java/io/flutter/packages/palettegenerator/imagecolors/MainActivity.java
@@ -0,0 +1,13 @@
+package io.flutter.packages.palettegenerator.imagecolors;
+
+import android.os.Bundle;
+import io.flutter.app.FlutterActivity;
+import io.flutter.plugins.GeneratedPluginRegistrant;
+
+public class MainActivity extends FlutterActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ GeneratedPluginRegistrant.registerWith(this);
+ }
+}
diff --git a/packages/palette_generator/example/android/app/src/main/kotlin/io/flutter/packages/palettegenerator/example/MainActivity.kt b/packages/palette_generator/example/android/app/src/main/kotlin/io/flutter/packages/palettegenerator/example/MainActivity.kt
new file mode 100644
index 0000000..e121e6c
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/kotlin/io/flutter/packages/palettegenerator/example/MainActivity.kt
@@ -0,0 +1,12 @@
+package io.flutter.packages.palettegenerator.imagecolors
+
+import androidx.annotation.NonNull;
+import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugins.GeneratedPluginRegistrant
+
+class MainActivity: FlutterActivity() {
+ override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
+ GeneratedPluginRegistrant.registerWith(flutterEngine);
+ }
+}
diff --git a/packages/palette_generator/example/android/app/src/main/res/drawable/launch_background.xml b/packages/palette_generator/example/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@android:color/white" />
+
+ <!-- You can insert your own image assets here -->
+ <!-- <item>
+ <bitmap
+ android:gravity="center"
+ android:src="@mipmap/launch_image" />
+ </item> -->
+</layer-list>
diff --git a/packages/palette_generator/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/palette_generator/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/palette_generator/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/palette_generator/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/palette_generator/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/palette_generator/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/palette_generator/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/palette_generator/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/palette_generator/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/palette_generator/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/palette_generator/example/android/app/src/main/res/values/styles.xml b/packages/palette_generator/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..00fa441
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+ <!-- Show a splash screen on the activity. Automatically removed when
+ Flutter draws its first frame -->
+ <item name="android:windowBackground">@drawable/launch_background</item>
+ </style>
+</resources>
diff --git a/packages/palette_generator/example/android/app/src/profile/AndroidManifest.xml b/packages/palette_generator/example/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..7b31ebf
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="io.flutter.packages.palettegenerator.imagecolors">
+ <!-- Flutter needs it to communicate with the running application
+ to allow setting breakpoints, to provide hot reload, etc.
+ -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
diff --git a/packages/palette_generator/example/android/build.gradle b/packages/palette_generator/example/android/build.gradle
new file mode 100644
index 0000000..e0d7ae2
--- /dev/null
+++ b/packages/palette_generator/example/android/build.gradle
@@ -0,0 +1,29 @@
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.5.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/palette_generator/example/android/gradle.properties b/packages/palette_generator/example/android/gradle.properties
new file mode 100644
index 0000000..38c8d45
--- /dev/null
+++ b/packages/palette_generator/example/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/palette_generator/example/android/gradle/wrapper/gradle-wrapper.jar b/packages/palette_generator/example/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
--- /dev/null
+++ b/packages/palette_generator/example/android/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/packages/palette_generator/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/palette_generator/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..380a29b
--- /dev/null
+++ b/packages/palette_generator/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Jan 07 08:46:39 PST 2020
+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/palette_generator/example/android/gradlew b/packages/palette_generator/example/android/gradlew
new file mode 100755
index 0000000..9d82f78
--- /dev/null
+++ b/packages/palette_generator/example/android/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/packages/palette_generator/example/android/gradlew.bat b/packages/palette_generator/example/android/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/packages/palette_generator/example/android/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/packages/palette_generator/example/android/settings.gradle b/packages/palette_generator/example/android/settings.gradle
new file mode 100644
index 0000000..5a2f14f
--- /dev/null
+++ b/packages/palette_generator/example/android/settings.gradle
@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+ pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+
+plugins.each { name, path ->
+ def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+ include ":$name"
+ project(":$name").projectDir = pluginDirectory
+}
diff --git a/packages/palette_generator/example/assets/landscape.png b/packages/palette_generator/example/assets/landscape.png
new file mode 100644
index 0000000..815f599
--- /dev/null
+++ b/packages/palette_generator/example/assets/landscape.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/.gitignore b/packages/palette_generator/example/ios/.gitignore
new file mode 100644
index 0000000..79cc4da
--- /dev/null
+++ b/packages/palette_generator/example/ios/.gitignore
@@ -0,0 +1,45 @@
+.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/app.flx
+/Flutter/app.zip
+/Flutter/flutter_assets/
+/Flutter/App.framework
+/Flutter/Flutter.framework
+/Flutter/Generated.xcconfig
+/ServiceDefinitions.json
+
+Pods/
+.symlinks/
diff --git a/packages/palette_generator/example/ios/Flutter/AppFrameworkInfo.plist b/packages/palette_generator/example/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..9367d48
--- /dev/null
+++ b/packages/palette_generator/example/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>App</string>
+ <key>CFBundleIdentifier</key>
+ <string>io.flutter.flutter.app</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>App</string>
+ <key>CFBundlePackageType</key>
+ <string>FMWK</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>MinimumOSVersion</key>
+ <string>8.0</string>
+</dict>
+</plist>
diff --git a/packages/palette_generator/example/ios/Flutter/Debug.xcconfig b/packages/palette_generator/example/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/packages/palette_generator/example/ios/Flutter/Debug.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/packages/palette_generator/example/ios/Flutter/Release.xcconfig b/packages/palette_generator/example/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/packages/palette_generator/example/ios/Flutter/Release.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/packages/palette_generator/example/ios/Flutter/flutter_export_environment.sh b/packages/palette_generator/example/ios/Flutter/flutter_export_environment.sh
new file mode 100755
index 0000000..d0df73c
--- /dev/null
+++ b/packages/palette_generator/example/ios/Flutter/flutter_export_environment.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+# This is a generated file; do not edit or check into version control.
+export "FLUTTER_ROOT=/Users/gspencer/code/flutter"
+export "FLUTTER_APPLICATION_PATH=/Users/gspencer/code/packages/packages/palette_generator/example"
+export "FLUTTER_TARGET=lib/main.dart"
+export "FLUTTER_BUILD_DIR=build"
+export "SYMROOT=${SOURCE_ROOT}/../build/ios"
+export "FLUTTER_FRAMEWORK_DIR=/Users/gspencer/code/flutter/bin/cache/artifacts/engine/ios"
+export "FLUTTER_BUILD_NAME=0.1.0"
+export "FLUTTER_BUILD_NUMBER=0.1.0"
diff --git a/packages/palette_generator/example/ios/Runner.xcodeproj/project.pbxproj b/packages/palette_generator/example/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..d78d489
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,438 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; };
+ 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, ); }; };
+ 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
+ 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; };
+ 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 = "<group>"; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
+ 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+ 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
+ 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
+ 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
+ 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
+ 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 = "<group>"; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
+ 3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 2D5378251FAA1A9400D5DBA9 /* flutter_assets */,
+ 3B80C3931E831B6300D905FE /* App.framework */,
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEBA1CF902C7004384FC /* Flutter.framework */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "<group>";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ CF3B75C9A7D2FA2A4C99F110 /* Frameworks */,
+ );
+ sourceTree = "<group>";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 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 = "<group>";
+ };
+ 97C146F11CF9000F007C117D /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146F21CF9000F007C117D /* main.m */,
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 0910;
+ ORGANIZATIONNAME = "The Chromium Authors";
+ TargetAttributes = {
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = English;
+ 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 */,
+ 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 2D5378261FAA1A9400D5DBA9 /* flutter_assets 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";
+ };
+/* 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 = "<group>";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 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_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_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_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_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)";
+ 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.packages.palette_generator.imageColors;
+ 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.packages.palette_generator.imageColors;
+ 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/palette_generator/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/palette_generator/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "group:Runner.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/palette_generator/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/palette_generator/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..1263ac8
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0910"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/packages/palette_generator/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/palette_generator/example/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "group:Runner.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/palette_generator/example/ios/Runner/AppDelegate.h b/packages/palette_generator/example/ios/Runner/AppDelegate.h
new file mode 100644
index 0000000..36e21bb
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/AppDelegate.h
@@ -0,0 +1,6 @@
+#import <Flutter/Flutter.h>
+#import <UIKit/UIKit.h>
+
+@interface AppDelegate : FlutterAppDelegate
+
+@end
diff --git a/packages/palette_generator/example/ios/Runner/AppDelegate.m b/packages/palette_generator/example/ios/Runner/AppDelegate.m
new file mode 100644
index 0000000..59a72e9
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/AppDelegate.m
@@ -0,0 +1,13 @@
+#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/palette_generator/example/ios/Runner/AppDelegate.swift b/packages/palette_generator/example/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..70693e4
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+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/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..3d43d11
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..28c6bf0
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..f091b6b
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cde121
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..d0ef06e
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..dcdc230
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..c8f9ed8
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..75b2d16
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..c4df70d
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..6a84f41
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..d0e1f58
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/packages/palette_generator/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/palette_generator/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
+ <viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
+ </imageView>
+ </subviews>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ <constraints>
+ <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
+ <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
+ </constraints>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+ <resources>
+ <image name="LaunchImage" width="168" height="185"/>
+ </resources>
+</document>
diff --git a/packages/palette_generator/example/ios/Runner/Base.lproj/Main.storyboard b/packages/palette_generator/example/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ </dependencies>
+ <scenes>
+ <!--Flutter View Controller-->
+ <scene sceneID="tne-QT-ifu">
+ <objects>
+ <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+ <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+ </objects>
+ </scene>
+ </scenes>
+</document>
diff --git a/packages/palette_generator/example/ios/Runner/Info.plist b/packages/palette_generator/example/ios/Runner/Info.plist
new file mode 100644
index 0000000..cdf4f69
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>image_colors</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>$(FLUTTER_BUILD_NAME)</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>$(FLUTTER_BUILD_NUMBER)</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UIViewControllerBasedStatusBarAppearance</key>
+ <false/>
+</dict>
+</plist>
diff --git a/packages/palette_generator/example/ios/Runner/Runner-Bridging-Header.h b/packages/palette_generator/example/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/packages/palette_generator/example/ios/Runner/main.m b/packages/palette_generator/example/ios/Runner/main.m
new file mode 100644
index 0000000..dff6597
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/main.m
@@ -0,0 +1,9 @@
+#import <Flutter/Flutter.h>
+#import <UIKit/UIKit.h>
+#import "AppDelegate.h"
+
+int main(int argc, char* argv[]) {
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
+ }
+}
diff --git a/packages/palette_generator/example/lib/main.dart b/packages/palette_generator/example/lib/main.dart
new file mode 100644
index 0000000..9e01739
--- /dev/null
+++ b/packages/palette_generator/example/lib/main.dart
@@ -0,0 +1,316 @@
+// 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:math' as math;
+
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+
+import 'package:palette_generator/palette_generator.dart';
+
+void main() => runApp(const MyApp());
+
+const Color _kBackgroundColor = Color(0xffa0a0a0);
+const Color _kSelectionRectangleBackground = Color(0x15000000);
+const Color _kSelectionRectangleBorder = Color(0x80000000);
+const Color _kPlaceholderColor = Color(0x80404040);
+
+/// The main Application class.
+class MyApp extends StatelessWidget {
+ /// Creates the main Application class.
+ const MyApp({Key? key}) : super(key: key);
+
+ // This widget is the root of your application.
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'Image Colors',
+ theme: ThemeData(
+ primarySwatch: Colors.green,
+ ),
+ home: const ImageColors(
+ title: 'Image Colors',
+ image: AssetImage('assets/landscape.png'),
+ imageSize: Size(256.0, 170.0),
+ ),
+ );
+ }
+}
+
+/// The home page for this example app.
+@immutable
+class ImageColors extends StatefulWidget {
+ /// Creates the home page.
+ const ImageColors({
+ Key? key,
+ this.title,
+ required this.image,
+ this.imageSize,
+ }) : super(key: key);
+
+ /// The title that is shown at the top of the page.
+ final String? title;
+
+ /// This is the image provider that is used to load the colors from.
+ final ImageProvider image;
+
+ /// The dimensions of the image.
+ final Size? imageSize;
+
+ @override
+ _ImageColorsState createState() {
+ return _ImageColorsState();
+ }
+}
+
+class _ImageColorsState extends State<ImageColors> {
+ Rect? region;
+ Rect? dragRegion;
+ Offset? startDrag;
+ Offset? currentDrag;
+ PaletteGenerator? paletteGenerator;
+
+ final GlobalKey imageKey = GlobalKey();
+
+ @override
+ void initState() {
+ super.initState();
+ if (widget.imageSize != null) {
+ region = Offset.zero & widget.imageSize!;
+ }
+ _updatePaletteGenerator(region);
+ }
+
+ Future<void> _updatePaletteGenerator(Rect? newRegion) async {
+ paletteGenerator = await PaletteGenerator.fromImageProvider(
+ widget.image,
+ size: widget.imageSize,
+ region: newRegion,
+ maximumColorCount: 20,
+ );
+ setState(() {});
+ }
+
+ // Called when the user starts to drag
+ void _onPanDown(DragDownDetails details) {
+ final RenderBox box =
+ imageKey.currentContext!.findRenderObject()! as RenderBox;
+ final Offset localPosition = box.globalToLocal(details.globalPosition);
+ setState(() {
+ startDrag = localPosition;
+ currentDrag = localPosition;
+ dragRegion = Rect.fromPoints(localPosition, localPosition);
+ });
+ }
+
+ // Called as the user drags: just updates the region, not the colors.
+ void _onPanUpdate(DragUpdateDetails details) {
+ setState(() {
+ currentDrag = currentDrag! + details.delta;
+ dragRegion = Rect.fromPoints(startDrag!, currentDrag!);
+ });
+ }
+
+ // Called if the drag is canceled (e.g. by rotating the device or switching
+ // apps)
+ void _onPanCancel() {
+ setState(() {
+ dragRegion = null;
+ startDrag = null;
+ });
+ }
+
+ // Called when the drag ends. Sets the region, and updates the colors.
+ Future<void> _onPanEnd(DragEndDetails details) async {
+ final Size? imageSize = imageKey.currentContext?.size;
+ Rect? newRegion;
+
+ if (imageSize != null) {
+ newRegion = (Offset.zero & imageSize).intersect(dragRegion!);
+ if (newRegion.size.width < 4 && newRegion.size.width < 4) {
+ newRegion = Offset.zero & imageSize;
+ }
+ }
+
+ await _updatePaletteGenerator(newRegion);
+ setState(() {
+ region = newRegion;
+ dragRegion = null;
+ startDrag = null;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: _kBackgroundColor,
+ appBar: AppBar(
+ title: Text(widget.title ?? ''),
+ ),
+ body: Column(
+ mainAxisSize: MainAxisSize.max,
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: <Widget>[
+ Padding(
+ padding: const EdgeInsets.all(20.0),
+ // GestureDetector is used to handle the selection rectangle.
+ child: GestureDetector(
+ onPanDown: _onPanDown,
+ onPanUpdate: _onPanUpdate,
+ onPanCancel: _onPanCancel,
+ onPanEnd: _onPanEnd,
+ child: Stack(children: <Widget>[
+ Image(
+ key: imageKey,
+ image: widget.image,
+ width: widget.imageSize?.width,
+ height: widget.imageSize?.height,
+ ),
+ // This is the selection rectangle.
+ Positioned.fromRect(
+ rect: dragRegion ?? region ?? Rect.zero,
+ child: Container(
+ decoration: BoxDecoration(
+ color: _kSelectionRectangleBackground,
+ border: Border.all(
+ width: 1.0,
+ color: _kSelectionRectangleBorder,
+ style: BorderStyle.solid,
+ )),
+ )),
+ ]),
+ ),
+ ),
+ // Use a FutureBuilder so that the palettes will be displayed when
+ // the palette generator is done generating its data.
+ PaletteSwatches(generator: paletteGenerator),
+ ],
+ ),
+ );
+ }
+}
+
+/// A widget that draws the swatches for the [PaletteGenerator] it is given,
+/// and shows the selected target colors.
+class PaletteSwatches extends StatelessWidget {
+ /// Create a Palette swatch.
+ ///
+ /// The [generator] is optional. If it is null, then the display will
+ /// just be an empty container.
+ const PaletteSwatches({Key? key, this.generator}) : super(key: key);
+
+ /// The [PaletteGenerator] that contains all of the swatches that we're going
+ /// to display.
+ final PaletteGenerator? generator;
+
+ @override
+ Widget build(BuildContext context) {
+ final List<Widget> swatches = <Widget>[];
+ final PaletteGenerator? paletteGen = generator;
+ if (paletteGen == null || paletteGen.colors.isEmpty) {
+ return Container();
+ }
+ for (final Color color in paletteGen.colors) {
+ swatches.add(PaletteSwatch(color: color));
+ }
+ return Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: <Widget>[
+ Wrap(
+ children: swatches,
+ ),
+ Container(height: 30.0),
+ PaletteSwatch(
+ label: 'Dominant', color: paletteGen.dominantColor?.color),
+ PaletteSwatch(
+ label: 'Light Vibrant', color: paletteGen.lightVibrantColor?.color),
+ PaletteSwatch(label: 'Vibrant', color: paletteGen.vibrantColor?.color),
+ PaletteSwatch(
+ label: 'Dark Vibrant', color: paletteGen.darkVibrantColor?.color),
+ PaletteSwatch(
+ label: 'Light Muted', color: paletteGen.lightMutedColor?.color),
+ PaletteSwatch(label: 'Muted', color: paletteGen.mutedColor?.color),
+ PaletteSwatch(
+ label: 'Dark Muted', color: paletteGen.darkMutedColor?.color),
+ ],
+ );
+ }
+}
+
+/// A small square of color with an optional label.
+@immutable
+class PaletteSwatch extends StatelessWidget {
+ /// Creates a PaletteSwatch.
+ ///
+ /// If the [paletteColor] has property `isTargetColorFound` as `false`,
+ /// then the swatch will show a placeholder instead, to indicate
+ /// that there is no color.
+ const PaletteSwatch({
+ Key? key,
+ this.color,
+ this.label,
+ }) : super(key: key);
+
+ /// The color of the swatch.
+ final Color? color;
+
+ /// The optional label to display next to the swatch.
+ final String? label;
+
+ @override
+ Widget build(BuildContext context) {
+ // Compute the "distance" of the color swatch and the background color
+ // so that we can put a border around those color swatches that are too
+ // close to the background's saturation and lightness. We ignore hue for
+ // the comparison.
+ final HSLColor hslColor = HSLColor.fromColor(color ?? Colors.transparent);
+ final HSLColor backgroundAsHsl = HSLColor.fromColor(_kBackgroundColor);
+ final double colorDistance = math.sqrt(
+ math.pow(hslColor.saturation - backgroundAsHsl.saturation, 2.0) +
+ math.pow(hslColor.lightness - backgroundAsHsl.lightness, 2.0));
+
+ Widget swatch = Padding(
+ padding: const EdgeInsets.all(2.0),
+ child: color == null
+ ? const Placeholder(
+ fallbackWidth: 34.0,
+ fallbackHeight: 20.0,
+ color: Color(0xff404040),
+ strokeWidth: 2.0,
+ )
+ : Container(
+ decoration: BoxDecoration(
+ color: color,
+ border: Border.all(
+ width: 1.0,
+ color: _kPlaceholderColor,
+ style: colorDistance < 0.2
+ ? BorderStyle.solid
+ : BorderStyle.none,
+ )),
+ width: 34.0,
+ height: 20.0,
+ ),
+ );
+
+ if (label != null) {
+ swatch = ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 130.0, minWidth: 130.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: <Widget>[
+ swatch,
+ Container(width: 5.0),
+ Text(label!),
+ ],
+ ),
+ );
+ }
+ return swatch;
+ }
+}
diff --git a/packages/palette_generator/example/macos/.gitignore b/packages/palette_generator/example/macos/.gitignore
new file mode 100644
index 0000000..d2fd377
--- /dev/null
+++ b/packages/palette_generator/example/macos/.gitignore
@@ -0,0 +1,6 @@
+# Flutter-related
+**/Flutter/ephemeral/
+**/Pods/
+
+# Xcode-related
+**/xcuserdata/
diff --git a/packages/palette_generator/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/palette_generator/example/macos/Flutter/Flutter-Debug.xcconfig
new file mode 100644
index 0000000..c2efd0b
--- /dev/null
+++ b/packages/palette_generator/example/macos/Flutter/Flutter-Debug.xcconfig
@@ -0,0 +1 @@
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/packages/palette_generator/example/macos/Flutter/Flutter-Release.xcconfig b/packages/palette_generator/example/macos/Flutter/Flutter-Release.xcconfig
new file mode 100644
index 0000000..c2efd0b
--- /dev/null
+++ b/packages/palette_generator/example/macos/Flutter/Flutter-Release.xcconfig
@@ -0,0 +1 @@
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/packages/palette_generator/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/palette_generator/example/macos/Flutter/GeneratedPluginRegistrant.swift
new file mode 100644
index 0000000..cccf817
--- /dev/null
+++ b/packages/palette_generator/example/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -0,0 +1,10 @@
+//
+// Generated file. Do not edit.
+//
+
+import FlutterMacOS
+import Foundation
+
+
+func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+}
diff --git a/packages/palette_generator/example/macos/Runner.xcodeproj/project.pbxproj b/packages/palette_generator/example/macos/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..331479d
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,596 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 51;
+ objects = {
+
+/* Begin PBXAggregateTarget section */
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
+ isa = PBXAggregateTarget;
+ buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
+ buildPhases = (
+ 33CC111E2044C6BF0003C045 /* ShellScript */,
+ );
+ dependencies = (
+ );
+ name = "Flutter Assemble";
+ productName = FLX;
+ };
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
+ 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; };
+ 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; };
+ D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC111A2044C6BA0003C045;
+ remoteInfo = FLX;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 33CC110E2044A8840003C045 /* Bundle Framework */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */,
+ 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */,
+ );
+ name = "Bundle Framework";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
+ 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 = "<group>"; };
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
+ 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
+ 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
+ 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 = "<group>"; };
+ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
+ 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 */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 33BA886A226E78AF003329D5 /* Configs */ = {
+ isa = PBXGroup;
+ children = (
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
+ );
+ path = Configs;
+ sourceTree = "<group>";
+ };
+ 33CC10E42044A3C60003C045 = {
+ isa = PBXGroup;
+ children = (
+ 33FAB671232836740065AC1E /* Runner */,
+ 33CEB47122A05771004F2AC0 /* Flutter */,
+ 33CC10EE2044A3C60003C045 /* Products */,
+ D73912EC22F37F3D000D13A0 /* Frameworks */,
+ );
+ sourceTree = "<group>";
+ };
+ 33CC10EE2044A3C60003C045 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10ED2044A3C60003C045 /* example.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 33CC11242044D66E0003C045 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */,
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */,
+ 33CC10F72044A3C60003C045 /* Info.plist */,
+ );
+ name = Resources;
+ path = ..;
+ sourceTree = "<group>";
+ };
+ 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 = "<group>";
+ };
+ 33FAB671232836740065AC1E /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */,
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */,
+ 33E51914231749380026EE4D /* Release.entitlements */,
+ 33CC11242044D66E0003C045 /* Resources */,
+ 33BA886A226E78AF003329D5 /* Configs */,
+ );
+ path = Runner;
+ sourceTree = "<group>";
+ };
+ D73912EC22F37F3D000D13A0 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 33CC10EC2044A3C60003C045 /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 33CC10E92044A3C60003C045 /* Sources */,
+ 33CC10EA2044A3C60003C045 /* Frameworks */,
+ 33CC10EB2044A3C60003C045 /* Resources */,
+ 33CC110E2044A8840003C045 /* Bundle Framework */,
+ 3399D490228B24CF009A79C7 /* ShellScript */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */,
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 33CC10ED2044A3C60003C045 /* example.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 33CC10E52044A3C60003C045 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 0920;
+ LastUpgradeCheck = 0930;
+ ORGANIZATIONNAME = "The Flutter Authors";
+ TargetAttributes = {
+ 33CC10EC2044A3C60003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ LastSwiftMigration = 1100;
+ ProvisioningStyle = Automatic;
+ SystemCapabilities = {
+ com.apple.Sandbox = {
+ enabled = 1;
+ };
+ };
+ };
+ 33CC111A2044C6BA0003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ ProvisioningStyle = Manual;
+ };
+ };
+ };
+ buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 8.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 33CC10E42044A3C60003C045;
+ productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 33CC10EC2044A3C60003C045 /* Runner */,
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 33CC10EB2044A3C60003C045 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3399D490228B24CF009A79C7 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\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";
+ };
+/* 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 = "<group>";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 338D0CE9231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.11;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Profile;
+ };
+ 338D0CEA231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter/ephemeral",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Profile;
+ };
+ 338D0CEB231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Profile;
+ };
+ 33CC10F92044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.11;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 33CC10FA2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.11;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Release;
+ };
+ 33CC10FC2044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter/ephemeral",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 33CC10FD2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter/ephemeral",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+ 33CC111C2044C6BA0003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ 33CC111D2044C6BA0003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10F92044A3C60003C045 /* Debug */,
+ 33CC10FA2044A3C60003C045 /* Release */,
+ 338D0CE9231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10FC2044A3C60003C045 /* Debug */,
+ 33CC10FD2044A3C60003C045 /* Release */,
+ 338D0CEA231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC111C2044C6BA0003C045 /* Debug */,
+ 33CC111D2044C6BA0003C045 /* Release */,
+ 338D0CEB231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 33CC10E52044A3C60003C045 /* Project object */;
+}
diff --git a/packages/palette_generator/example/macos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/palette_generator/example/macos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..764c74b
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "self:/Users/stuartmorgan/src/embedder-opensource/flutter-desktop-embedding/example/macos/Runner.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/palette_generator/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/palette_generator/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IDEDidComputeMac32BitWarning</key>
+ <true/>
+</dict>
+</plist>
diff --git a/packages/palette_generator/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/palette_generator/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..df12c33
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "1000"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "33CC10EC2044A3C60003C045"
+ BuildableName = "example.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "00380F9121DF178D00097171"
+ BuildableName = "RunnerUITests.xctest"
+ BlueprintName = "RunnerUITests"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "33CC10EC2044A3C60003C045"
+ BuildableName = "example.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "33CC10EC2044A3C60003C045"
+ BuildableName = "example.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "33CC10EC2044A3C60003C045"
+ BuildableName = "example.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/packages/palette_generator/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/palette_generator/example/macos/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "group:Runner.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/palette_generator/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/palette_generator/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IDEDidComputeMac32BitWarning</key>
+ <true/>
+</dict>
+</plist>
diff --git a/packages/palette_generator/example/macos/Runner/AppDelegate.swift b/packages/palette_generator/example/macos/Runner/AppDelegate.swift
new file mode 100644
index 0000000..d53ef64
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/AppDelegate.swift
@@ -0,0 +1,9 @@
+import Cocoa
+import FlutterMacOS
+
+@NSApplicationMain
+class AppDelegate: FlutterAppDelegate {
+ override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+ return true
+ }
+}
diff --git a/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..a2ec33f
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_16.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_64.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_128.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_1024.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
new file mode 100644
index 0000000..3c4935a
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
Binary files differ
diff --git a/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
new file mode 100644
index 0000000..ed4cc16
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
Binary files differ
diff --git a/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
new file mode 100644
index 0000000..483be61
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
Binary files differ
diff --git a/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
new file mode 100644
index 0000000..bcbf36d
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
Binary files differ
diff --git a/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
new file mode 100644
index 0000000..9c0a652
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
Binary files differ
diff --git a/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
new file mode 100644
index 0000000..e71a726
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
Binary files differ
diff --git a/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
new file mode 100644
index 0000000..8a31fe2
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
Binary files differ
diff --git a/packages/palette_generator/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/palette_generator/example/macos/Runner/Base.lproj/MainMenu.xib
new file mode 100644
index 0000000..537341a
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Base.lproj/MainMenu.xib
@@ -0,0 +1,339 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
+ <dependencies>
+ <deployment identifier="macosx"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <objects>
+ <customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
+ <connections>
+ <outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
+ </connections>
+ </customObject>
+ <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
+ <customObject id="-3" userLabel="Application" customClass="NSObject"/>
+ <customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
+ <connections>
+ <outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
+ <outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
+ </connections>
+ </customObject>
+ <customObject id="YLy-65-1bz" customClass="NSFontManager"/>
+ <menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
+ <items>
+ <menuItem title="APP_NAME" id="1Xt-HY-uBw">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
+ <items>
+ <menuItem title="About APP_NAME" id="5kV-Vb-QxS">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
+ <menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
+ <menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
+ <menuItem title="Services" id="NMo-om-nkz">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
+ <menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
+ <connections>
+ <action selector="hide:" target="-1" id="PnN-Uc-m68"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
+ <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+ <connections>
+ <action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Show All" id="Kd2-mp-pUS">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
+ <menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
+ <connections>
+ <action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Edit" id="5QF-Oa-p0T">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Edit" id="W48-6f-4Dl">
+ <items>
+ <menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
+ <connections>
+ <action selector="undo:" target="-1" id="M6e-cu-g7V"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
+ <connections>
+ <action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
+ <menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
+ <connections>
+ <action selector="cut:" target="-1" id="YJe-68-I9s"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
+ <connections>
+ <action selector="copy:" target="-1" id="G1f-GL-Joy"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
+ <connections>
+ <action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
+ <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+ <connections>
+ <action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Delete" id="pa3-QI-u2k">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
+ <connections>
+ <action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
+ <menuItem title="Find" id="4EN-yA-p0u">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Find" id="1b7-l0-nxx">
+ <items>
+ <menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
+ <connections>
+ <action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
+ <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+ <connections>
+ <action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
+ <connections>
+ <action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
+ <connections>
+ <action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
+ <connections>
+ <action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
+ <connections>
+ <action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
+ <items>
+ <menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
+ <connections>
+ <action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
+ <connections>
+ <action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
+ <menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Substitutions" id="9ic-FL-obx">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
+ <items>
+ <menuItem title="Show Substitutions" id="z6F-FW-3nz">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
+ <menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Smart Quotes" id="hQb-2v-fYv">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Smart Dashes" id="rgM-f4-ycn">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Smart Links" id="cwL-P1-jid">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Data Detectors" id="tRr-pd-1PS">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Text Replacement" id="HFQ-gK-NFA">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Transformations" id="2oI-Rn-ZJC">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Transformations" id="c8a-y6-VQd">
+ <items>
+ <menuItem title="Make Upper Case" id="vmV-6d-7jI">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Make Lower Case" id="d9M-CD-aMd">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Capitalize" id="UEZ-Bs-lqG">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Speech" id="xrE-MZ-jX0">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Speech" id="3rS-ZA-NoH">
+ <items>
+ <menuItem title="Start Speaking" id="Ynk-f8-cLZ">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Stop Speaking" id="Oyz-dy-DGm">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="View" id="H8h-7b-M4v">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="View" id="HyV-fh-RgO">
+ <items>
+ <menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
+ <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
+ <connections>
+ <action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Window" id="aUF-d1-5bR">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
+ <items>
+ <menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
+ <connections>
+ <action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Zoom" id="R4o-n2-Eq4">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
+ <menuItem title="Bring All to Front" id="LE2-aR-0XJ">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ </items>
+ <point key="canvasLocation" x="142" y="-258"/>
+ </menu>
+ <window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
+ <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
+ <rect key="contentRect" x="335" y="390" width="800" height="600"/>
+ <rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
+ <view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
+ <rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
+ <autoresizingMask key="autoresizingMask"/>
+ </view>
+ </window>
+ </objects>
+</document>
diff --git a/packages/palette_generator/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/palette_generator/example/macos/Runner/Configs/AppInfo.xcconfig
new file mode 100644
index 0000000..380aa95
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Configs/AppInfo.xcconfig
@@ -0,0 +1,14 @@
+// Application-level settings for the Runner target.
+//
+// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
+// future. If not, the values below would default to using the project name when this becomes a
+// 'flutter create' template.
+
+// The application's name. By default this is also the title of the Flutter window.
+PRODUCT_NAME = example
+
+// The application's bundle identifier
+PRODUCT_BUNDLE_IDENTIFIER = io.flutter.packages.palettegenerator.imagecolors
+
+// The copyright displayed in application information
+PRODUCT_COPYRIGHT = Copyright © 2020 io.flutter.packages.palettegenerator. All rights reserved.
diff --git a/packages/palette_generator/example/macos/Runner/Configs/Debug.xcconfig b/packages/palette_generator/example/macos/Runner/Configs/Debug.xcconfig
new file mode 100644
index 0000000..36b0fd9
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Configs/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Debug.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/packages/palette_generator/example/macos/Runner/Configs/Release.xcconfig b/packages/palette_generator/example/macos/Runner/Configs/Release.xcconfig
new file mode 100644
index 0000000..dff4f49
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Configs/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Release.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/packages/palette_generator/example/macos/Runner/Configs/Warnings.xcconfig b/packages/palette_generator/example/macos/Runner/Configs/Warnings.xcconfig
new file mode 100644
index 0000000..42bcbf4
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Configs/Warnings.xcconfig
@@ -0,0 +1,13 @@
+WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
+GCC_WARN_UNDECLARED_SELECTOR = YES
+CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
+CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
+CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
+CLANG_WARN_PRAGMA_PACK = YES
+CLANG_WARN_STRICT_PROTOTYPES = YES
+CLANG_WARN_COMMA = YES
+GCC_WARN_STRICT_SELECTOR_MATCH = YES
+CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
+CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
+GCC_WARN_SHADOW = YES
+CLANG_WARN_UNREACHABLE_CODE = YES
diff --git a/packages/palette_generator/example/macos/Runner/DebugProfile.entitlements b/packages/palette_generator/example/macos/Runner/DebugProfile.entitlements
new file mode 100644
index 0000000..dddb8a3
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/DebugProfile.entitlements
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>com.apple.security.app-sandbox</key>
+ <true/>
+ <key>com.apple.security.cs.allow-jit</key>
+ <true/>
+ <key>com.apple.security.network.server</key>
+ <true/>
+</dict>
+</plist>
diff --git a/packages/palette_generator/example/macos/Runner/Info.plist b/packages/palette_generator/example/macos/Runner/Info.plist
new file mode 100644
index 0000000..4789daa
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Info.plist
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIconFile</key>
+ <string></string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>$(FLUTTER_BUILD_NAME)</string>
+ <key>CFBundleVersion</key>
+ <string>$(FLUTTER_BUILD_NUMBER)</string>
+ <key>LSMinimumSystemVersion</key>
+ <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
+ <key>NSHumanReadableCopyright</key>
+ <string>$(PRODUCT_COPYRIGHT)</string>
+ <key>NSMainNibFile</key>
+ <string>MainMenu</string>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+</dict>
+</plist>
diff --git a/packages/palette_generator/example/macos/Runner/MainFlutterWindow.swift b/packages/palette_generator/example/macos/Runner/MainFlutterWindow.swift
new file mode 100644
index 0000000..2722837
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/MainFlutterWindow.swift
@@ -0,0 +1,15 @@
+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/palette_generator/example/macos/Runner/Release.entitlements b/packages/palette_generator/example/macos/Runner/Release.entitlements
new file mode 100644
index 0000000..852fa1a
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Release.entitlements
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>com.apple.security.app-sandbox</key>
+ <true/>
+</dict>
+</plist>
diff --git a/packages/palette_generator/example/pubspec.yaml b/packages/palette_generator/example/pubspec.yaml
new file mode 100644
index 0000000..b2b0cbd
--- /dev/null
+++ b/packages/palette_generator/example/pubspec.yaml
@@ -0,0 +1,22 @@
+name: image_colors
+description: A simple example of how to use the PaletteGenerator to load the palette from an image file.
+publish_to: none
+version: 0.1.0
+
+environment:
+ sdk: ">=2.12.0 <3.0.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+ palette_generator:
+ path: ../
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+flutter:
+ uses-material-design: true
+ assets:
+ - assets/landscape.png
diff --git a/packages/palette_generator/lib/palette_generator.dart b/packages/palette_generator/lib/palette_generator.dart
new file mode 100644
index 0000000..ed721f5
--- /dev/null
+++ b/packages/palette_generator/lib/palette_generator.dart
@@ -0,0 +1,1274 @@
+// 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.
+
+// TODO(goderbauer): Fix this warning for the classes in this file.
+// ignore_for_file: avoid_equals_and_hash_code_on_mutable_classes
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:math' as math;
+import 'dart:typed_data';
+import 'dart:ui' as ui;
+import 'dart:ui' show Color;
+
+import 'package:collection/collection.dart'
+ show PriorityQueue, HeapPriorityQueue;
+import 'package:flutter/foundation.dart';
+import 'package:flutter/painting.dart';
+
+/// A class to extract prominent colors from an image for use as user interface
+/// colors.
+///
+/// To create a new [PaletteGenerator], use the asynchronous
+/// [PaletteGenerator.fromImage] static function.
+///
+/// A number of color paletteColors with different profiles are chosen from the
+/// image:
+///
+/// * [vibrantColor]
+/// * [darkVibrantColor]
+/// * [lightVibrantColor]
+/// * [mutedColor]
+/// * [darkMutedColor]
+/// * [lightMutedColor]
+///
+/// You may add your own target palette color types by supplying them to the
+/// `targets` parameter for [PaletteGenerator.fromImage].
+///
+/// In addition, the population-sorted list of discovered [colors] is available,
+/// and a [paletteColors] list providing contrasting title text and body text
+/// colors for each palette color.
+///
+/// The palette is created using a color quantizer based on the Median-cut
+/// algorithm, but optimized for picking out distinct colors rather than
+/// representative colors.
+///
+/// The color space is represented as a 3-dimensional cube with each dimension
+/// being one component of an RGB image. The cube is then repeatedly divided
+/// until the color space is reduced to the requested number of colors. An
+/// average color is then generated from each cube.
+///
+/// What makes this different from a median-cut algorithm is that median-cut
+/// divides cubes so that all of the cubes have roughly the same population,
+/// where the quantizer that is used to create the palette divides cubes based
+/// on their color volume. This means that the color space is divided into
+/// distinct colors, rather than representative colors.
+///
+/// See also:
+///
+/// * [PaletteColor], to contain various pieces of metadata about a chosen
+/// palette color.
+/// * [PaletteTarget], to be able to create your own target color types.
+/// * [PaletteFilter], a function signature for filtering the allowed colors
+/// in the palette.
+class PaletteGenerator with Diagnosticable {
+ /// Create a [PaletteGenerator] from a set of paletteColors and targets.
+ ///
+ /// The usual way to create a [PaletteGenerator] is to use the asynchronous
+ /// [PaletteGenerator.fromImage] static function. This constructor is mainly
+ /// used for cases when you have your own source of color information and
+ /// would like to use the target selection and scoring methods here.
+ PaletteGenerator.fromColors(
+ this.paletteColors, {
+ this.targets = const <PaletteTarget>[],
+ }) : selectedSwatches = <PaletteTarget, PaletteColor>{} {
+ _sortSwatches();
+ _selectSwatches();
+ }
+
+ /// Create a [PaletteGenerator] from an [dart:ui.Image] asynchronously.
+ ///
+ /// The [region] specifies the part of the image to inspect for color
+ /// candidates. By default it uses the entire image. Must not be equal to
+ /// [Rect.zero], and must not be larger than the image dimensions.
+ ///
+ /// The [maximumColorCount] sets the maximum number of colors that will be
+ /// returned in the [PaletteGenerator]. The default is 16 colors.
+ ///
+ /// The [filters] specify a lost of [PaletteFilter] instances that can be used
+ /// to include certain colors in the list of colors. The default filter is
+ /// an instance of [AvoidRedBlackWhitePaletteFilter], which stays away from
+ /// whites, blacks, and low-saturation reds.
+ ///
+ /// The [targets] are a list of target color types, specified by creating
+ /// custom [PaletteTarget]s. By default, this is the list of targets in
+ /// [PaletteTarget.baseTargets].
+ static Future<PaletteGenerator> fromImage(
+ ui.Image image, {
+ Rect? region,
+ int maximumColorCount = _defaultCalculateNumberColors,
+ List<PaletteFilter> filters = const <PaletteFilter>[
+ avoidRedBlackWhitePaletteFilter
+ ],
+ List<PaletteTarget> targets = const <PaletteTarget>[],
+ }) async {
+ assert(region == null || region != Rect.zero);
+ assert(
+ region == null ||
+ (region.topLeft.dx >= 0.0 && region.topLeft.dy >= 0.0),
+ 'Region $region is outside the image ${image.width}x${image.height}');
+ assert(
+ region == null ||
+ (region.bottomRight.dx <= image.width &&
+ region.bottomRight.dy <= image.height),
+ 'Region $region is outside the image ${image.width}x${image.height}');
+
+ final _ColorCutQuantizer quantizer = _ColorCutQuantizer(
+ image,
+ maxColors: maximumColorCount,
+ filters: filters,
+ region: region,
+ );
+ final List<PaletteColor> colors = await quantizer.quantizedColors;
+ return PaletteGenerator.fromColors(
+ colors,
+ targets: targets,
+ );
+ }
+
+ /// Create a [PaletteGenerator] from an [ImageProvider], like [FileImage], or
+ /// [AssetImage], asynchronously.
+ ///
+ /// The [size] is the desired size of the image. The image will be resized to
+ /// this size before creating the [PaletteGenerator] from it.
+ ///
+ /// The [region] specifies the part of the (resized) image to inspect for
+ /// color candidates. By default it uses the entire image. Must not be equal
+ /// to [Rect.zero], and must not be larger than the image dimensions.
+ ///
+ /// The [maximumColorCount] sets the maximum number of colors that will be
+ /// returned in the [PaletteGenerator]. The default is 16 colors.
+ ///
+ /// The [filters] specify a lost of [PaletteFilter] instances that can be used
+ /// to include certain colors in the list of colors. The default filter is
+ /// an instance of [AvoidRedBlackWhitePaletteFilter], which stays away from
+ /// whites, blacks, and low-saturation reds.
+ ///
+ /// The [targets] are a list of target color types, specified by creating
+ /// custom [PaletteTarget]s. By default, this is the list of targets in
+ /// [PaletteTarget.baseTargets].
+ ///
+ /// The [timeout] describes how long to wait for the image to load before
+ /// giving up on it. A value of Duration.zero implies waiting forever. The
+ /// default timeout is 15 seconds.
+ static Future<PaletteGenerator> fromImageProvider(
+ ImageProvider imageProvider, {
+ Size? size,
+ Rect? region,
+ int maximumColorCount = _defaultCalculateNumberColors,
+ List<PaletteFilter> filters = const <PaletteFilter>[
+ avoidRedBlackWhitePaletteFilter
+ ],
+ List<PaletteTarget> targets = const <PaletteTarget>[],
+ Duration timeout = const Duration(seconds: 15),
+ }) async {
+ assert(region == null || size != null);
+ assert(region == null || region != Rect.zero);
+ assert(
+ region == null ||
+ (region.topLeft.dx >= 0.0 && region.topLeft.dy >= 0.0),
+ 'Region $region is outside the image ${size!.width}x${size.height}');
+ assert(region == null || size!.contains(region.topLeft),
+ 'Region $region is outside the image $size');
+ assert(
+ region == null ||
+ (region.bottomRight.dx <= size!.width &&
+ region.bottomRight.dy <= size.height),
+ 'Region $region is outside the image $size');
+ final ImageStream stream = imageProvider.resolve(
+ ImageConfiguration(size: size, devicePixelRatio: 1.0),
+ );
+ final Completer<ui.Image> imageCompleter = Completer<ui.Image>();
+ Timer? loadFailureTimeout;
+ late ImageStreamListener listener;
+ listener = ImageStreamListener((ImageInfo info, bool synchronousCall) {
+ loadFailureTimeout?.cancel();
+ stream.removeListener(listener);
+ imageCompleter.complete(info.image);
+ });
+
+ if (timeout != Duration.zero) {
+ loadFailureTimeout = Timer(timeout, () {
+ stream.removeListener(listener);
+ imageCompleter.completeError(
+ TimeoutException(
+ 'Timeout occurred trying to load from $imageProvider'),
+ );
+ });
+ }
+ stream.addListener(listener);
+ final ui.Image image = await imageCompleter.future;
+ ui.Rect? newRegion = region;
+ if (size != null && region != null) {
+ final double scale = image.width / size.width;
+ newRegion = Rect.fromLTRB(
+ region.left * scale,
+ region.top * scale,
+ region.right * scale,
+ region.bottom * scale,
+ );
+ }
+ return PaletteGenerator.fromImage(
+ image,
+ region: newRegion,
+ maximumColorCount: maximumColorCount,
+ filters: filters,
+ targets: targets,
+ );
+ }
+
+ static const int _defaultCalculateNumberColors = 16;
+
+ /// Provides a map of the selected paletteColors for each target in [targets].
+ final Map<PaletteTarget, PaletteColor> selectedSwatches;
+
+ /// The list of [PaletteColor]s that make up the palette, sorted from most
+ /// dominant color to least dominant color.
+ final List<PaletteColor> paletteColors;
+
+ /// The list of targets that the palette uses for custom color selection.
+ ///
+ /// By default, this contains the entire list of predefined targets in
+ /// [PaletteTarget.baseTargets].
+ final List<PaletteTarget> targets;
+
+ /// Returns a list of colors in the [paletteColors], sorted from most
+ /// dominant to least dominant color.
+ Iterable<Color> get colors sync* {
+ for (final PaletteColor paletteColor in paletteColors) {
+ yield paletteColor.color;
+ }
+ }
+
+ /// Returns a vibrant color from the palette. Might be null if an appropriate
+ /// target color could not be found.
+ PaletteColor? get vibrantColor => selectedSwatches[PaletteTarget.vibrant];
+
+ /// Returns a light and vibrant color from the palette. Might be null if an
+ /// appropriate target color could not be found.
+ PaletteColor? get lightVibrantColor =>
+ selectedSwatches[PaletteTarget.lightVibrant];
+
+ /// Returns a dark and vibrant color from the palette. Might be null if an
+ /// appropriate target color could not be found.
+ PaletteColor? get darkVibrantColor =>
+ selectedSwatches[PaletteTarget.darkVibrant];
+
+ /// Returns a muted color from the palette. Might be null if an appropriate
+ /// target color could not be found.
+ PaletteColor? get mutedColor => selectedSwatches[PaletteTarget.muted];
+
+ /// Returns a muted and light color from the palette. Might be null if an
+ /// appropriate target color could not be found.
+ PaletteColor? get lightMutedColor =>
+ selectedSwatches[PaletteTarget.lightMuted];
+
+ /// Returns a muted and dark color from the palette. Might be null if an
+ /// appropriate target color could not be found.
+ PaletteColor? get darkMutedColor => selectedSwatches[PaletteTarget.darkMuted];
+
+ /// The dominant color (the color with the largest population).
+ PaletteColor? get dominantColor => _dominantColor;
+ PaletteColor? _dominantColor;
+
+ void _sortSwatches() {
+ if (paletteColors.isEmpty) {
+ _dominantColor = null;
+ return;
+ }
+ // Sort from most common to least common.
+ paletteColors.sort((PaletteColor a, PaletteColor b) {
+ return b.population.compareTo(a.population);
+ });
+ _dominantColor = paletteColors[0];
+ }
+
+ void _selectSwatches() {
+ final Set<PaletteTarget> allTargets =
+ Set<PaletteTarget>.from(targets + PaletteTarget.baseTargets);
+ final Set<Color> usedColors = <Color>{};
+ for (final PaletteTarget target in allTargets) {
+ target._normalizeWeights();
+ final PaletteColor? targetScore =
+ _generateScoredTarget(target, usedColors);
+ if (targetScore != null) {
+ selectedSwatches[target] = targetScore;
+ }
+ }
+ }
+
+ PaletteColor? _generateScoredTarget(
+ PaletteTarget target, Set<Color> usedColors) {
+ final PaletteColor? maxScoreSwatch =
+ _getMaxScoredSwatchForTarget(target, usedColors);
+ if (maxScoreSwatch != null && target.isExclusive) {
+ // If we have a color, and the target is exclusive, add the color to the
+ // used list.
+ usedColors.add(maxScoreSwatch.color);
+ }
+ return maxScoreSwatch;
+ }
+
+ PaletteColor? _getMaxScoredSwatchForTarget(
+ PaletteTarget target, Set<Color> usedColors) {
+ double maxScore = 0.0;
+ PaletteColor? maxScoreSwatch;
+ for (final PaletteColor paletteColor in paletteColors) {
+ if (_shouldBeScoredForTarget(paletteColor, target, usedColors)) {
+ final double score = _generateScore(paletteColor, target);
+ if (maxScoreSwatch == null || score > maxScore) {
+ maxScoreSwatch = paletteColor;
+ maxScore = score;
+ }
+ }
+ }
+ return maxScoreSwatch;
+ }
+
+ bool _shouldBeScoredForTarget(
+ PaletteColor paletteColor, PaletteTarget target, Set<Color> usedColors) {
+ // Check whether the HSL lightness is within the correct range, and that
+ // this color hasn't been used yet.
+ final HSLColor hslColor = HSLColor.fromColor(paletteColor.color);
+ return hslColor.saturation >= target.minimumSaturation &&
+ hslColor.saturation <= target.maximumSaturation &&
+ hslColor.lightness >= target.minimumLightness &&
+ hslColor.lightness <= target.maximumLightness &&
+ !usedColors.contains(paletteColor.color);
+ }
+
+ double _generateScore(PaletteColor paletteColor, PaletteTarget target) {
+ final HSLColor hslColor = HSLColor.fromColor(paletteColor.color);
+
+ double saturationScore = 0.0;
+ double valueScore = 0.0;
+ double populationScore = 0.0;
+
+ if (target.saturationWeight > 0.0) {
+ saturationScore = target.saturationWeight *
+ (1.0 - (hslColor.saturation - target.targetSaturation).abs());
+ }
+ if (target.lightnessWeight > 0.0) {
+ valueScore = target.lightnessWeight *
+ (1.0 - (hslColor.lightness - target.targetLightness).abs());
+ }
+ if (_dominantColor != null && target.populationWeight > 0.0) {
+ populationScore = target.populationWeight *
+ (paletteColor.population / _dominantColor!.population);
+ }
+
+ return saturationScore + valueScore + populationScore;
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(IterableProperty<PaletteColor>(
+ 'paletteColors', paletteColors,
+ defaultValue: <PaletteColor>[]));
+ properties.add(IterableProperty<PaletteTarget>('targets', targets,
+ defaultValue: PaletteTarget.baseTargets));
+ }
+}
+
+/// A class which allows custom selection of colors when a [PaletteGenerator] is
+/// generated.
+///
+/// To add a target, supply it to the `targets` list in
+/// [PaletteGenerator.fromImage] or [PaletteGenerator..fromColors].
+///
+/// See also:
+///
+/// * [PaletteGenerator], a class for selecting color palettes from images.
+class PaletteTarget with Diagnosticable {
+ /// Creates a [PaletteTarget] for custom palette selection.
+ PaletteTarget({
+ this.minimumSaturation = 0.0,
+ this.targetSaturation = 0.5,
+ this.maximumSaturation = 1.0,
+ this.minimumLightness = 0.0,
+ this.targetLightness = 0.5,
+ this.maximumLightness = 1.0,
+ this.isExclusive = true,
+ });
+
+ /// The minimum saturation value for this target.
+ final double minimumSaturation;
+
+ /// The target saturation value for this target.
+ final double targetSaturation;
+
+ /// The maximum saturation value for this target.
+ final double maximumSaturation;
+
+ /// The minimum lightness value for this target.
+ final double minimumLightness;
+
+ /// The target lightness value for this target.
+ final double targetLightness;
+
+ /// The maximum lightness value for this target.
+ final double maximumLightness;
+
+ /// Returns whether any color selected for this target is exclusive for this
+ /// target only.
+ ///
+ /// If false, then the color can also be selected for other targets. Defaults
+ /// to true.
+ final bool isExclusive;
+
+ /// The weight of importance that a color's saturation value has on selection.
+ double saturationWeight = _weightSaturation;
+
+ /// The weight of importance that a color's lightness value has on selection.
+ double lightnessWeight = _weightLightness;
+
+ /// The weight of importance that a color's population value has on selection.
+ double populationWeight = _weightPopulation;
+
+ static const double _targetDarkLightness = 0.26;
+ static const double _maxDarkLightness = 0.45;
+
+ static const double _minLightLightness = 0.55;
+ static const double _targetLightLightness = 0.74;
+
+ static const double _minNormalLightness = 0.3;
+ static const double _targetNormalLightness = 0.5;
+ static const double _maxNormalLightness = 0.7;
+
+ static const double _targetMutedSaturation = 0.3;
+ static const double _maxMutedSaturation = 0.4;
+
+ static const double _targetVibrantSaturation = 1.0;
+ static const double _minVibrantSaturation = 0.35;
+
+ static const double _weightSaturation = 0.24;
+ static const double _weightLightness = 0.52;
+ static const double _weightPopulation = 0.24;
+
+ /// A target which has the characteristics of a vibrant color which is light
+ /// in luminance.
+ ///
+ /// One of the base set of `targets` for [PaletteGenerator.fromImage], in [baseTargets].
+ static final PaletteTarget lightVibrant = PaletteTarget(
+ targetLightness: _targetLightLightness,
+ minimumLightness: _minLightLightness,
+ minimumSaturation: _minVibrantSaturation,
+ targetSaturation: _targetVibrantSaturation,
+ );
+
+ /// A target which has the characteristics of a vibrant color which is neither
+ /// light or dark.
+ ///
+ /// One of the base set of `targets` for [PaletteGenerator.fromImage], in [baseTargets].
+ static final PaletteTarget vibrant = PaletteTarget(
+ minimumLightness: _minNormalLightness,
+ targetLightness: _targetNormalLightness,
+ maximumLightness: _maxNormalLightness,
+ minimumSaturation: _minVibrantSaturation,
+ targetSaturation: _targetVibrantSaturation,
+ );
+
+ /// A target which has the characteristics of a vibrant color which is dark in
+ /// luminance.
+ ///
+ /// One of the base set of `targets` for [PaletteGenerator.fromImage], in [baseTargets].
+ static final PaletteTarget darkVibrant = PaletteTarget(
+ targetLightness: _targetDarkLightness,
+ maximumLightness: _maxDarkLightness,
+ minimumSaturation: _minVibrantSaturation,
+ targetSaturation: _targetVibrantSaturation,
+ );
+
+ /// A target which has the characteristics of a muted color which is light in
+ /// luminance.
+ ///
+ /// One of the base set of `targets` for [PaletteGenerator.fromImage], in [baseTargets].
+ static final PaletteTarget lightMuted = PaletteTarget(
+ targetLightness: _targetLightLightness,
+ minimumLightness: _minLightLightness,
+ targetSaturation: _targetMutedSaturation,
+ maximumSaturation: _maxMutedSaturation,
+ );
+
+ /// A target which has the characteristics of a muted color which is neither
+ /// light or dark.
+ ///
+ /// One of the base set of `targets` for [PaletteGenerator.fromImage], in [baseTargets].
+ static final PaletteTarget muted = PaletteTarget(
+ minimumLightness: _minNormalLightness,
+ targetLightness: _targetNormalLightness,
+ maximumLightness: _maxNormalLightness,
+ targetSaturation: _targetMutedSaturation,
+ maximumSaturation: _maxMutedSaturation,
+ );
+
+ /// A target which has the characteristics of a muted color which is dark in
+ /// luminance.
+ ///
+ /// One of the base set of `targets` for [PaletteGenerator.fromImage], in [baseTargets].
+ static final PaletteTarget darkMuted = PaletteTarget(
+ targetLightness: _targetDarkLightness,
+ maximumLightness: _maxDarkLightness,
+ targetSaturation: _targetMutedSaturation,
+ maximumSaturation: _maxMutedSaturation,
+ );
+
+ /// A list of all the available predefined targets.
+ ///
+ /// The base set of `targets` for [PaletteGenerator.fromImage].
+ static final List<PaletteTarget> baseTargets = <PaletteTarget>[
+ lightVibrant,
+ vibrant,
+ darkVibrant,
+ lightMuted,
+ muted,
+ darkMuted,
+ ];
+
+ void _normalizeWeights() {
+ final double sum = saturationWeight + lightnessWeight + populationWeight;
+ if (sum != 0.0) {
+ saturationWeight /= sum;
+ lightnessWeight /= sum;
+ populationWeight /= sum;
+ }
+ }
+
+ @override
+ bool operator ==(dynamic other) {
+ return minimumSaturation == other.minimumSaturation &&
+ targetSaturation == other.targetSaturation &&
+ maximumSaturation == other.maximumSaturation &&
+ minimumLightness == other.minimumLightness &&
+ targetLightness == other.targetLightness &&
+ maximumLightness == other.maximumLightness &&
+ saturationWeight == other.saturationWeight &&
+ lightnessWeight == other.lightnessWeight &&
+ populationWeight == other.populationWeight;
+ }
+
+ @override
+ int get hashCode {
+ return hashValues(
+ minimumSaturation,
+ targetSaturation,
+ maximumSaturation,
+ minimumLightness,
+ targetLightness,
+ maximumLightness,
+ saturationWeight,
+ lightnessWeight,
+ populationWeight,
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ final PaletteTarget defaultTarget = PaletteTarget();
+ properties.add(DoubleProperty('minimumSaturation', minimumSaturation,
+ defaultValue: defaultTarget.minimumSaturation));
+ properties.add(DoubleProperty('targetSaturation', targetSaturation,
+ defaultValue: defaultTarget.targetSaturation));
+ properties.add(DoubleProperty('maximumSaturation', maximumSaturation,
+ defaultValue: defaultTarget.maximumSaturation));
+ properties.add(DoubleProperty('minimumLightness', minimumLightness,
+ defaultValue: defaultTarget.minimumLightness));
+ properties.add(DoubleProperty('targetLightness', targetLightness,
+ defaultValue: defaultTarget.targetLightness));
+ properties.add(DoubleProperty('maximumLightness', maximumLightness,
+ defaultValue: defaultTarget.maximumLightness));
+ properties.add(DoubleProperty('saturationWeight', saturationWeight,
+ defaultValue: defaultTarget.saturationWeight));
+ properties.add(DoubleProperty('lightnessWeight', lightnessWeight,
+ defaultValue: defaultTarget.lightnessWeight));
+ properties.add(DoubleProperty('populationWeight', populationWeight,
+ defaultValue: defaultTarget.populationWeight));
+ }
+}
+
+typedef _ContrastCalculator = double Function(Color a, Color b, int alpha);
+
+/// A color palette color generated by the [PaletteGenerator].
+///
+/// This palette color represents a dominant [color] in an image, and has a
+/// [population] of how many pixels in the source image it represents. It picks
+/// a [titleTextColor] and a [bodyTextColor] that contrast sufficiently with the
+/// source [color] for comfortable reading.
+///
+/// See also:
+///
+/// * [PaletteGenerator], a class for selecting color palettes from images.
+class PaletteColor with Diagnosticable {
+ /// Generate a [PaletteColor].
+ PaletteColor(this.color, this.population);
+
+ static const double _minContrastTitleText = 3.0;
+ static const double _minContrastBodyText = 4.5;
+
+ /// The color that this palette color represents.
+ final Color color;
+
+ /// The number of pixels in the source image that this palette color
+ /// represents.
+ final int population;
+
+ /// The color of title text for use with this palette color.
+ Color get titleTextColor {
+ if (_titleTextColor == null) {
+ _ensureTextColorsGenerated();
+ }
+ return _titleTextColor!;
+ }
+
+ Color? _titleTextColor;
+
+ /// The color of body text for use with this palette color.
+ Color get bodyTextColor {
+ if (_bodyTextColor == null) {
+ _ensureTextColorsGenerated();
+ }
+ return _bodyTextColor!;
+ }
+
+ Color? _bodyTextColor;
+
+ void _ensureTextColorsGenerated() {
+ if (_titleTextColor == null || _bodyTextColor == null) {
+ const Color white = Color(0xffffffff);
+ const Color black = Color(0xff000000);
+ // First check white, as most colors will be dark
+ final int? lightBodyAlpha =
+ _calculateMinimumAlpha(white, color, _minContrastBodyText);
+ final int? lightTitleAlpha =
+ _calculateMinimumAlpha(white, color, _minContrastTitleText);
+
+ if (lightBodyAlpha != null && lightTitleAlpha != null) {
+ // If we found valid light values, use them and return
+ _bodyTextColor = white.withAlpha(lightBodyAlpha);
+ _titleTextColor = white.withAlpha(lightTitleAlpha);
+ return;
+ }
+
+ final int? darkBodyAlpha =
+ _calculateMinimumAlpha(black, color, _minContrastBodyText);
+ final int? darkTitleAlpha =
+ _calculateMinimumAlpha(black, color, _minContrastTitleText);
+
+ if (darkBodyAlpha != null && darkTitleAlpha != null) {
+ // If we found valid dark values, use them and return
+ _bodyTextColor = black.withAlpha(darkBodyAlpha);
+ _titleTextColor = black.withAlpha(darkTitleAlpha);
+ return;
+ }
+
+ // If we reach here then we can not find title and body values which use
+ // the same lightness, we need to use mismatched values
+ _bodyTextColor = lightBodyAlpha != null
+ ? white.withAlpha(lightBodyAlpha)
+ : black.withAlpha(darkBodyAlpha ?? 255);
+ _titleTextColor = lightTitleAlpha != null
+ ? white.withAlpha(lightTitleAlpha)
+ : black.withAlpha(darkTitleAlpha ?? 255);
+ }
+ }
+
+ /// Returns the contrast ratio between [foreground] and [background].
+ /// [background] must be opaque.
+ ///
+ /// Formula defined [here](http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef).
+ static double _calculateContrast(Color foreground, Color background) {
+ assert(background.alpha == 0xff,
+ 'background can not be translucent: $background.');
+ if (foreground.alpha < 0xff) {
+ // If the foreground is translucent, composite the foreground over the
+ // background
+ foreground = Color.alphaBlend(foreground, background);
+ }
+ final double lightness1 = foreground.computeLuminance() + 0.05;
+ final double lightness2 = background.computeLuminance() + 0.05;
+ return math.max(lightness1, lightness2) / math.min(lightness1, lightness2);
+ }
+
+ // Calculates the minimum alpha value which can be applied to foreground that
+ // would have a contrast value of at least [minContrastRatio] when compared to
+ // background.
+ //
+ // The background must be opaque (alpha of 255).
+ //
+ // Returns the alpha value in the range 0-255, or null if no value could be
+ // calculated.
+ static int? _calculateMinimumAlpha(
+ Color foreground, Color background, double minContrastRatio) {
+ assert(background.alpha == 0xff,
+ 'The background cannot be translucent: $background.');
+ double contrastCalculator(Color fg, Color bg, int alpha) {
+ final Color testForeground = fg.withAlpha(alpha);
+ return _calculateContrast(testForeground, bg);
+ }
+
+ // First lets check that a fully opaque foreground has sufficient contrast
+ final double testRatio = contrastCalculator(foreground, background, 0xff);
+ if (testRatio < minContrastRatio) {
+ // Fully opaque foreground does not have sufficient contrast, return error
+ return null;
+ }
+ foreground = foreground.withAlpha(0xff);
+ return _binaryAlphaSearch(
+ foreground, background, minContrastRatio, contrastCalculator);
+ }
+
+ // Calculates the alpha value using binary search based on a given contrast
+ // evaluation function and target contrast that needs to be satisfied.
+ //
+ // The background must be opaque (alpha of 255).
+ //
+ // Returns the alpha value in the range [0, 255].
+ static int _binaryAlphaSearch(
+ Color foreground,
+ Color background,
+ double minContrastRatio,
+ _ContrastCalculator calculator,
+ ) {
+ assert(background.alpha == 0xff,
+ 'The background cannot be translucent: $background.');
+ const int minAlphaSearchMaxIterations = 10;
+ const int minAlphaSearchPrecision = 1;
+
+ // Binary search to find a value with the minimum value which provides
+ // sufficient contrast
+ int numIterations = 0;
+ int minAlpha = 0;
+ int maxAlpha = 0xff;
+ while (numIterations <= minAlphaSearchMaxIterations &&
+ (maxAlpha - minAlpha) > minAlphaSearchPrecision) {
+ final int testAlpha = (minAlpha + maxAlpha) ~/ 2;
+ final double testRatio = calculator(foreground, background, testAlpha);
+ if (testRatio < minContrastRatio) {
+ minAlpha = testAlpha;
+ } else {
+ maxAlpha = testAlpha;
+ }
+ numIterations++;
+ }
+ // Conservatively return the max of the range of possible alphas, which is
+ // known to pass.
+ return maxAlpha;
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty<Color>('color', color));
+ properties
+ .add(DiagnosticsProperty<Color>('titleTextColor', titleTextColor));
+ properties.add(DiagnosticsProperty<Color>('bodyTextColor', bodyTextColor));
+ properties.add(IntProperty('population', population, defaultValue: 0));
+ }
+
+ @override
+ int get hashCode {
+ return hashValues(color, population);
+ }
+
+ @override
+ bool operator ==(dynamic other) {
+ return color == other.color && population == other.population;
+ }
+}
+
+/// Hook to allow clients to be able filter colors from selected in a
+/// [PaletteGenerator]. Returns true if the [color] is allowed.
+///
+/// See also:
+///
+/// * [PaletteGenerator.fromImage], which takes a list of these for its
+/// `filters` parameter.
+/// * [avoidRedBlackWhitePaletteFilter], the default filter for
+/// [PaletteGenerator].
+typedef PaletteFilter = bool Function(HSLColor color);
+
+/// A basic [PaletteFilter], which rejects colors near black, white and low
+/// saturation red.
+///
+/// Use this as an element in the `filters` list given to [PaletteGenerator.fromImage].
+///
+/// See also:
+/// * [PaletteGenerator], a class for selecting color palettes from images.
+bool avoidRedBlackWhitePaletteFilter(HSLColor color) {
+ bool _isBlack(HSLColor hslColor) {
+ const double _blackMaxLightness = 0.05;
+ return hslColor.lightness <= _blackMaxLightness;
+ }
+
+ bool _isWhite(HSLColor hslColor) {
+ const double _whiteMinLightness = 0.95;
+ return hslColor.lightness >= _whiteMinLightness;
+ }
+
+ // Returns true if the color is close to the red side of the I line.
+ bool _isNearRedILine(HSLColor hslColor) {
+ const double redLineMinHue = 10.0;
+ const double redLineMaxHue = 37.0;
+ const double redLineMaxSaturation = 0.82;
+ return hslColor.hue >= redLineMinHue &&
+ hslColor.hue <= redLineMaxHue &&
+ hslColor.saturation <= redLineMaxSaturation;
+ }
+
+ return !_isWhite(color) && !_isBlack(color) && !_isNearRedILine(color);
+}
+
+enum _ColorComponent {
+ red,
+ green,
+ blue,
+}
+
+/// A box that represents a volume in the RGB color space.
+class _ColorVolumeBox {
+ _ColorVolumeBox(
+ this._lowerIndex, this._upperIndex, this.histogram, this.colors) {
+ _fitMinimumBox();
+ }
+
+ final _ColorHistogram histogram;
+ final List<Color> colors;
+
+ // The lower and upper index are inclusive.
+ final int _lowerIndex;
+ int _upperIndex;
+
+ // The population of colors within this box.
+ late int _population;
+
+ // Bounds in each of the dimensions.
+ late int _minRed;
+ late int _maxRed;
+ late int _minGreen;
+ late int _maxGreen;
+ late int _minBlue;
+ late int _maxBlue;
+
+ int getVolume() {
+ return (_maxRed - _minRed + 1) *
+ (_maxGreen - _minGreen + 1) *
+ (_maxBlue - _minBlue + 1);
+ }
+
+ bool canSplit() {
+ return getColorCount() > 1;
+ }
+
+ int getColorCount() {
+ return 1 + _upperIndex - _lowerIndex;
+ }
+
+ /// Recomputes the boundaries of this box to tightly fit the colors within the
+ /// box.
+ void _fitMinimumBox() {
+ // Reset the min and max to opposite values
+ int minRed = 256;
+ int minGreen = 256;
+ int minBlue = 256;
+ int maxRed = -1;
+ int maxGreen = -1;
+ int maxBlue = -1;
+ int count = 0;
+ for (int i = _lowerIndex; i <= _upperIndex; i++) {
+ final Color color = colors[i];
+ count += histogram[color]!.value;
+ if (color.red > maxRed) {
+ maxRed = color.red;
+ }
+ if (color.red < minRed) {
+ minRed = color.red;
+ }
+ if (color.green > maxGreen) {
+ maxGreen = color.green;
+ }
+ if (color.green < minGreen) {
+ minGreen = color.green;
+ }
+ if (color.blue > maxBlue) {
+ maxBlue = color.blue;
+ }
+ if (color.blue < minBlue) {
+ minBlue = color.blue;
+ }
+ }
+ _minRed = minRed;
+ _maxRed = maxRed;
+ _minGreen = minGreen;
+ _maxGreen = maxGreen;
+ _minBlue = minBlue;
+ _maxBlue = maxBlue;
+ _population = count;
+ }
+
+ /// Split this color box at the mid-point along it's longest dimension.
+ ///
+ /// Returns the new ColorBox.
+ _ColorVolumeBox splitBox() {
+ assert(canSplit(), "Can't split a box with only 1 color");
+ // find median along the longest dimension
+ final int splitPoint = _findSplitPoint();
+ final _ColorVolumeBox newBox =
+ _ColorVolumeBox(splitPoint + 1, _upperIndex, histogram, colors);
+ // Now change this box's upperIndex and recompute the color boundaries
+ _upperIndex = splitPoint;
+ _fitMinimumBox();
+ return newBox;
+ }
+
+ /// Returns the largest dimension of this color box.
+ _ColorComponent _getLongestColorDimension() {
+ final int redLength = _maxRed - _minRed;
+ final int greenLength = _maxGreen - _minGreen;
+ final int blueLength = _maxBlue - _minBlue;
+ if (redLength >= greenLength && redLength >= blueLength) {
+ return _ColorComponent.red;
+ } else if (greenLength >= redLength && greenLength >= blueLength) {
+ return _ColorComponent.green;
+ } else {
+ return _ColorComponent.blue;
+ }
+ }
+
+ // Finds where to split this box between _lowerIndex and _upperIndex.
+ //
+ // The split point is calculated by finding the longest color dimension, and
+ // then sorting the sub-array based on that dimension value in each color.
+ // The colors are then iterated over until a color is found with the
+ // midpoint closest to the whole box's dimension midpoint.
+ //
+ // Returns the index of the split point in the colors array.
+ int _findSplitPoint() {
+ final _ColorComponent longestDimension = _getLongestColorDimension();
+ int compareColors(Color a, Color b) {
+ int makeValue(int first, int second, int third) {
+ return first << 16 | second << 8 | third;
+ }
+
+ switch (longestDimension) {
+ case _ColorComponent.red:
+ final int aValue = makeValue(a.red, a.green, a.blue);
+ final int bValue = makeValue(b.red, b.green, b.blue);
+ return aValue.compareTo(bValue);
+ case _ColorComponent.green:
+ final int aValue = makeValue(a.green, a.red, a.blue);
+ final int bValue = makeValue(b.green, b.red, b.blue);
+ return aValue.compareTo(bValue);
+ case _ColorComponent.blue:
+ final int aValue = makeValue(a.blue, a.green, a.red);
+ final int bValue = makeValue(b.blue, b.green, b.red);
+ return aValue.compareTo(bValue);
+ }
+ }
+
+ // We need to sort the colors in this box based on the longest color
+ // dimension.
+ final List<Color> colorSubset =
+ colors.sublist(_lowerIndex, _upperIndex + 1);
+ colorSubset.sort(compareColors);
+ colors.replaceRange(_lowerIndex, _upperIndex + 1, colorSubset);
+ final int median = (_population / 2).round();
+ for (int i = 0, count = 0; i <= colorSubset.length; i++) {
+ count += histogram[colorSubset[i]]!.value;
+ if (count >= median) {
+ // We never want to split on the upperIndex, as this will result in the
+ // same box.
+ return math.min(_upperIndex - 1, i + _lowerIndex);
+ }
+ }
+ return _lowerIndex;
+ }
+
+ PaletteColor getAverageColor() {
+ int redSum = 0;
+ int greenSum = 0;
+ int blueSum = 0;
+ int totalPopulation = 0;
+ for (int i = _lowerIndex; i <= _upperIndex; i++) {
+ final Color color = colors[i];
+ final int colorPopulation = histogram[color]!.value;
+ totalPopulation += colorPopulation;
+ redSum += colorPopulation * color.red;
+ greenSum += colorPopulation * color.green;
+ blueSum += colorPopulation * color.blue;
+ }
+ final int redMean = (redSum / totalPopulation).round();
+ final int greenMean = (greenSum / totalPopulation).round();
+ final int blueMean = (blueSum / totalPopulation).round();
+ return PaletteColor(
+ Color.fromARGB(0xff, redMean, greenMean, blueMean),
+ totalPopulation,
+ );
+ }
+}
+
+/// Holds mutable count for a color.
+// Using a mutable count rather than replacing value in the histogram
+// in the _ColorCutQuantizer speeds up building the histogram significantly.
+class _ColorCount {
+ int value = 0;
+}
+
+class _ColorHistogram {
+ final Map<int, Map<int, Map<int, _ColorCount>>> _hist =
+ <int, Map<int, Map<int, _ColorCount>>>{};
+ final DoubleLinkedQueue<Color> _keys = DoubleLinkedQueue<Color>();
+
+ _ColorCount? operator [](Color color) {
+ final Map<int, Map<int, _ColorCount>>? redMap = _hist[color.red];
+ if (redMap == null) {
+ return null;
+ }
+ final Map<int, _ColorCount>? blueMap = redMap[color.blue];
+ if (blueMap == null) {
+ return null;
+ }
+ return blueMap[color.green];
+ }
+
+ void operator []=(Color key, _ColorCount value) {
+ final int red = key.red;
+ final int blue = key.blue;
+ final int green = key.green;
+
+ bool newColor = false;
+
+ Map<int, Map<int, _ColorCount>>? redMap = _hist[red];
+ if (redMap == null) {
+ _hist[red] = redMap = <int, Map<int, _ColorCount>>{};
+ newColor = true;
+ }
+
+ Map<int, _ColorCount>? blueMap = redMap[blue];
+ if (blueMap == null) {
+ redMap[blue] = blueMap = <int, _ColorCount>{};
+ newColor = true;
+ }
+
+ if (blueMap[green] == null) {
+ newColor = true;
+ }
+ blueMap[green] = value;
+
+ if (newColor) {
+ _keys.add(key);
+ }
+ }
+
+ void removeWhere(bool Function(Color key) predicate) {
+ for (final Color key in _keys) {
+ if (predicate(key)) {
+ _hist[key.red]?[key.blue]?.remove(key.green);
+ }
+ }
+ _keys.removeWhere((Color color) => predicate(color));
+ }
+
+ Iterable<Color> get keys {
+ return _keys;
+ }
+
+ int get length {
+ return _keys.length;
+ }
+}
+
+class _ColorCutQuantizer {
+ _ColorCutQuantizer(
+ this.image, {
+ this.maxColors = PaletteGenerator._defaultCalculateNumberColors,
+ this.region,
+ this.filters = const <PaletteFilter>[avoidRedBlackWhitePaletteFilter],
+ }) : assert(region == null || region != Rect.zero),
+ _paletteColors = <PaletteColor>[];
+
+ FutureOr<List<PaletteColor>> get quantizedColors async {
+ if (_paletteColors.isNotEmpty) {
+ return _paletteColors;
+ } else {
+ return _quantizeColors(image);
+ }
+ }
+
+ final ui.Image image;
+ final List<PaletteColor> _paletteColors;
+
+ final int maxColors;
+ final Rect? region;
+ final List<PaletteFilter> filters;
+
+ Iterable<Color> _getImagePixels(ByteData pixels, int width, int height,
+ {Rect? region}) sync* {
+ final int rowStride = width * 4;
+ int rowStart;
+ int rowEnd;
+ int colStart;
+ int colEnd;
+ if (region != null) {
+ rowStart = region.top.floor();
+ rowEnd = region.bottom.floor();
+ colStart = region.left.floor();
+ colEnd = region.right.floor();
+ assert(rowStart >= 0);
+ assert(rowEnd <= height);
+ assert(colStart >= 0);
+ assert(colEnd <= width);
+ } else {
+ rowStart = 0;
+ rowEnd = height;
+ colStart = 0;
+ colEnd = width;
+ }
+ int byteCount = 0;
+ for (int row = rowStart; row < rowEnd; ++row) {
+ for (int col = colStart; col < colEnd; ++col) {
+ final int position = row * rowStride + col * 4;
+ // Convert from RGBA to ARGB.
+ final int pixel = pixels.getUint32(position);
+ final Color result = Color((pixel << 24) | (pixel >> 8));
+ byteCount += 4;
+ yield result;
+ }
+ }
+ assert(byteCount == ((rowEnd - rowStart) * (colEnd - colStart) * 4));
+ }
+
+ bool _shouldIgnoreColor(Color color) {
+ final HSLColor hslColor = HSLColor.fromColor(color);
+ if (filters.isNotEmpty) {
+ for (final PaletteFilter filter in filters) {
+ if (!filter(hslColor)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ Future<List<PaletteColor>> _quantizeColors(ui.Image image) async {
+ const int quantizeWordWidth = 5;
+ const int quantizeChannelWidth = 8;
+ const int quantizeShift = quantizeChannelWidth - quantizeWordWidth;
+ const int quantizeWordMask =
+ ((1 << quantizeWordWidth) - 1) << quantizeShift;
+
+ Color quantizeColor(Color color) {
+ return Color.fromARGB(
+ color.alpha,
+ color.red & quantizeWordMask,
+ color.green & quantizeWordMask,
+ color.blue & quantizeWordMask,
+ );
+ }
+
+ final ByteData? imageData =
+ await image.toByteData(format: ui.ImageByteFormat.rawRgba);
+ if (imageData == null) {
+ throw 'Failed to encode the image.';
+ }
+ final Iterable<Color> pixels =
+ _getImagePixels(imageData, image.width, image.height, region: region);
+ final _ColorHistogram hist = _ColorHistogram();
+ Color? currentColor;
+ _ColorCount? currentColorCount;
+
+ for (final Color pixel in pixels) {
+ // Update the histogram, but only for non-zero alpha values, and for the
+ // ones we do add, make their alphas opaque so that we can use a Color as
+ // the histogram key.
+ final Color quantizedColor = quantizeColor(pixel);
+ final Color colorKey = quantizedColor.withAlpha(0xff);
+ // Skip pixels that are entirely transparent.
+ if (quantizedColor.alpha == 0x0) {
+ continue;
+ }
+ if (currentColor != colorKey) {
+ currentColor = colorKey;
+ currentColorCount = hist[colorKey];
+ if (currentColorCount == null) {
+ hist[colorKey] = currentColorCount = _ColorCount();
+ }
+ }
+ currentColorCount!.value = currentColorCount.value + 1;
+ }
+ // Now let's remove any colors that the filters want to ignore.
+ hist.removeWhere((Color color) {
+ return _shouldIgnoreColor(color);
+ });
+ if (hist.length <= maxColors) {
+ // The image has fewer colors than the maximum requested, so just return
+ // the colors.
+ _paletteColors.clear();
+ for (final Color color in hist.keys) {
+ _paletteColors.add(PaletteColor(color, hist[color]!.value));
+ }
+ } else {
+ // We need use quantization to reduce the number of colors
+ _paletteColors.clear();
+ _paletteColors.addAll(_quantizePixels(maxColors, hist));
+ }
+ return _paletteColors;
+ }
+
+ List<PaletteColor> _quantizePixels(
+ int maxColors,
+ _ColorHistogram histogram,
+ ) {
+ int volumeComparator(_ColorVolumeBox a, _ColorVolumeBox b) {
+ return b.getVolume().compareTo(a.getVolume());
+ }
+
+ // Create the priority queue which is sorted by volume descending. This
+ // means we always split the largest box in the queue
+ final PriorityQueue<_ColorVolumeBox> priorityQueue =
+ HeapPriorityQueue<_ColorVolumeBox>(volumeComparator);
+ // To start, offer a box which contains all of the colors
+ priorityQueue.add(_ColorVolumeBox(
+ 0, histogram.length - 1, histogram, histogram.keys.toList()));
+ // Now go through the boxes, splitting them until we have reached maxColors
+ // or there are no more boxes to split
+ _splitBoxes(priorityQueue, maxColors);
+ // Finally, return the average colors of the color boxes.
+ return _generateAverageColors(priorityQueue);
+ }
+
+ // Iterate through the [PriorityQueue], popping [_ColorVolumeBox] objects
+ // from the queue and splitting them. Once split, the new box and the
+ // remaining box are offered back to the queue.
+ //
+ // The `maxSize` is the maximum number of boxes to split.
+ void _splitBoxes(PriorityQueue<_ColorVolumeBox> queue, final int maxSize) {
+ while (queue.length < maxSize) {
+ final _ColorVolumeBox colorVolumeBox = queue.removeFirst();
+ if (colorVolumeBox.canSplit()) {
+ // First split the box, and offer the result
+ queue.add(colorVolumeBox.splitBox());
+ // Then offer the box back
+ queue.add(colorVolumeBox);
+ } else {
+ // If we get here then there are no more boxes to split, so return
+ return;
+ }
+ }
+ }
+
+ // Generates the average colors from each of the boxes in the queue.
+ List<PaletteColor> _generateAverageColors(
+ PriorityQueue<_ColorVolumeBox> colorVolumeBoxes) {
+ final List<PaletteColor> colors = <PaletteColor>[];
+ for (final _ColorVolumeBox colorVolumeBox in colorVolumeBoxes.toList()) {
+ final PaletteColor paletteColor = colorVolumeBox.getAverageColor();
+ if (!_shouldIgnoreColor(paletteColor.color)) {
+ colors.add(paletteColor);
+ }
+ }
+ return colors;
+ }
+}
diff --git a/packages/palette_generator/palette_generator.iml b/packages/palette_generator/palette_generator.iml
new file mode 100644
index 0000000..c9ef74c
--- /dev/null
+++ b/packages/palette_generator/palette_generator.iml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
+ <excludeFolder url="file://$MODULE_DIR$/.pub" />
+ <excludeFolder url="file://$MODULE_DIR$/build" />
+ <excludeFolder url="file://$MODULE_DIR$/example/.dart_tool" />
+ <excludeFolder url="file://$MODULE_DIR$/example/.pub" />
+ <excludeFolder url="file://$MODULE_DIR$/example/android/app/src/main/java/io/flutter/packages" />
+ <excludeFolder url="file://$MODULE_DIR$/example/build" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="library" name="Dart SDK" level="project" />
+ </component>
+</module>
\ No newline at end of file
diff --git a/packages/palette_generator/pubspec.yaml b/packages/palette_generator/pubspec.yaml
new file mode 100644
index 0000000..8bc401b
--- /dev/null
+++ b/packages/palette_generator/pubspec.yaml
@@ -0,0 +1,18 @@
+name: palette_generator
+description: Flutter package for generating palette colors from a source image.
+homepage: https://github.com/flutter/packages/tree/master/packages/palette_generator
+version: 0.3.0
+
+environment:
+ sdk: ">=2.12.0 <3.0.0"
+ flutter: ">=1.15.21"
+
+dependencies:
+ collection: ^1.15.0
+ flutter:
+ sdk: flutter
+ path: ^1.8.0
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
diff --git a/packages/palette_generator/test/assets/dominant.png b/packages/palette_generator/test/assets/dominant.png
new file mode 100644
index 0000000..ca54543
--- /dev/null
+++ b/packages/palette_generator/test/assets/dominant.png
Binary files differ
diff --git a/packages/palette_generator/test/assets/landscape.png b/packages/palette_generator/test/assets/landscape.png
new file mode 100644
index 0000000..815f599
--- /dev/null
+++ b/packages/palette_generator/test/assets/landscape.png
Binary files differ
diff --git a/packages/palette_generator/test/assets/tall_blue.png b/packages/palette_generator/test/assets/tall_blue.png
new file mode 100644
index 0000000..6453d79
--- /dev/null
+++ b/packages/palette_generator/test/assets/tall_blue.png
Binary files differ
diff --git a/packages/palette_generator/test/assets/wide_red.png b/packages/palette_generator/test/assets/wide_red.png
new file mode 100644
index 0000000..5130b44
--- /dev/null
+++ b/packages/palette_generator/test/assets/wide_red.png
Binary files differ
diff --git a/packages/palette_generator/test/palette_generator_test.dart b/packages/palette_generator/test/palette_generator_test.dart
new file mode 100644
index 0000000..375b5db
--- /dev/null
+++ b/packages/palette_generator/test/palette_generator_test.dart
@@ -0,0 +1,388 @@
+// 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:io';
+import 'dart:typed_data';
+import 'dart:ui' as ui show Image, Codec, FrameInfo, instantiateImageCodec;
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/foundation.dart';
+import 'package:path/path.dart' as path;
+import 'package:palette_generator/palette_generator.dart';
+
+/// An image provider implementation for testing that takes a pre-loaded image.
+/// This avoids handling asynchronous I/O in the test zone, which is
+/// problematic.
+class FakeImageProvider extends ImageProvider<FakeImageProvider> {
+ const FakeImageProvider(this._image, {this.scale = 1.0});
+
+ final ui.Image _image;
+
+ /// The scale to place in the [ImageInfo] object of the image.
+ final double scale;
+
+ @override
+ Future<FakeImageProvider> obtainKey(ImageConfiguration configuration) {
+ return SynchronousFuture<FakeImageProvider>(this);
+ }
+
+ @override
+ ImageStreamCompleter load(FakeImageProvider key, DecoderCallback decode) {
+ assert(key == this);
+ return OneFrameImageStreamCompleter(
+ SynchronousFuture<ImageInfo>(
+ ImageInfo(image: _image, scale: scale),
+ ),
+ );
+ }
+}
+
+Future<ImageProvider> loadImage(String name) async {
+ File imagePath = File(path.joinAll(<String>['assets', name]));
+ if (path.split(Directory.current.absolute.path).last != 'test') {
+ imagePath = File(path.join('test', imagePath.path));
+ }
+ final Uint8List data = Uint8List.fromList(imagePath.readAsBytesSync());
+ final ui.Codec codec = await ui.instantiateImageCodec(data);
+ final ui.FrameInfo frameInfo = await codec.getNextFrame();
+ return FakeImageProvider(frameInfo.image);
+}
+
+Future<void> main() async {
+ // Load the images outside of the test zone so that IO doesn't get
+ // complicated.
+ final List<String> imageNames = <String>[
+ 'tall_blue',
+ 'wide_red',
+ 'dominant',
+ 'landscape'
+ ];
+ final Map<String, ImageProvider> testImages = <String, ImageProvider>{};
+ for (final String name in imageNames) {
+ testImages[name] = await loadImage('$name.png');
+ }
+
+ testWidgets('Initialize the image cache', (WidgetTester tester) async {
+ // We need to have a testWidgets test in order to initialize the image
+ // cache for the other tests, but they timeout if they too are testWidgets
+ // tests.
+ tester.pumpWidget(const Placeholder());
+ });
+
+ test('PaletteGenerator works on 1-pixel wide blue image', () async {
+ final PaletteGenerator palette =
+ await PaletteGenerator.fromImageProvider(testImages['tall_blue']!);
+ expect(palette.paletteColors.length, equals(1));
+ expect(palette.paletteColors[0].color,
+ within<Color>(distance: 8, from: const Color(0xff0000ff)));
+ });
+
+ test('PaletteGenerator works on 1-pixel high red image', () async {
+ final PaletteGenerator palette =
+ await PaletteGenerator.fromImageProvider(testImages['wide_red']!);
+ expect(palette.paletteColors.length, equals(1));
+ expect(palette.paletteColors[0].color,
+ within<Color>(distance: 8, from: const Color(0xffff0000)));
+ });
+
+ test('PaletteGenerator finds dominant color and text colors', () async {
+ final PaletteGenerator palette =
+ await PaletteGenerator.fromImageProvider(testImages['dominant']!);
+ expect(palette.paletteColors.length, equals(3));
+ expect(palette.dominantColor, isNotNull);
+ expect(palette.dominantColor!.color,
+ within<Color>(distance: 8, from: const Color(0xff0000ff)));
+ expect(palette.dominantColor!.titleTextColor,
+ within<Color>(distance: 8, from: const Color(0x8affffff)));
+ expect(palette.dominantColor!.bodyTextColor,
+ within<Color>(distance: 8, from: const Color(0xb2ffffff)));
+ });
+
+ test('PaletteGenerator works with regions', () async {
+ final ImageProvider imageProvider = testImages['dominant']!;
+ Rect region = const Rect.fromLTRB(0.0, 0.0, 100.0, 100.0);
+ const Size size = Size(100.0, 100.0);
+ PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
+ imageProvider,
+ region: region,
+ size: size);
+ expect(palette.paletteColors.length, equals(3));
+ expect(palette.dominantColor, isNotNull);
+ expect(palette.dominantColor!.color,
+ within<Color>(distance: 8, from: const Color(0xff0000ff)));
+
+ region = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0);
+ palette = await PaletteGenerator.fromImageProvider(imageProvider,
+ region: region, size: size);
+ expect(palette.paletteColors.length, equals(1));
+ expect(palette.dominantColor, isNotNull);
+ expect(palette.dominantColor!.color,
+ within<Color>(distance: 8, from: const Color(0xffff0000)));
+
+ region = const Rect.fromLTRB(0.0, 0.0, 30.0, 20.0);
+ palette = await PaletteGenerator.fromImageProvider(imageProvider,
+ region: region, size: size);
+ expect(palette.paletteColors.length, equals(3));
+ expect(palette.dominantColor, isNotNull);
+ expect(palette.dominantColor!.color,
+ within<Color>(distance: 8, from: const Color(0xff00ff00)));
+ });
+
+ test('PaletteGenerator works as expected on a real image', () async {
+ final PaletteGenerator palette =
+ await PaletteGenerator.fromImageProvider(testImages['landscape']!);
+ final List<PaletteColor> expectedSwatches = <PaletteColor>[
+ PaletteColor(const Color(0xff3f630c), 10137),
+ PaletteColor(const Color(0xff3c4b2a), 4773),
+ PaletteColor(const Color(0xff81b2e9), 4762),
+ PaletteColor(const Color(0xffc0d6ec), 4714),
+ PaletteColor(const Color(0xff4c4f50), 2465),
+ PaletteColor(const Color(0xff5c635b), 2463),
+ PaletteColor(const Color(0xff6e80a2), 2421),
+ PaletteColor(const Color(0xff9995a3), 1214),
+ PaletteColor(const Color(0xff676c4d), 1213),
+ PaletteColor(const Color(0xffc4b2b2), 1173),
+ PaletteColor(const Color(0xff445166), 1040),
+ PaletteColor(const Color(0xff475d83), 1019),
+ PaletteColor(const Color(0xff7e7360), 589),
+ PaletteColor(const Color(0xfff6b835), 286),
+ PaletteColor(const Color(0xffb9983d), 152),
+ PaletteColor(const Color(0xffe3ab35), 149),
+ ];
+ final Iterable<Color> expectedColors =
+ expectedSwatches.map<Color>((PaletteColor swatch) => swatch.color);
+ expect(palette.paletteColors, containsAll(expectedSwatches));
+ expect(palette.vibrantColor, isNotNull);
+ expect(palette.lightVibrantColor, isNotNull);
+ expect(palette.darkVibrantColor, isNotNull);
+ expect(palette.mutedColor, isNotNull);
+ expect(palette.lightMutedColor, isNotNull);
+ expect(palette.darkMutedColor, isNotNull);
+ expect(palette.vibrantColor!.color,
+ within<Color>(distance: 8, from: const Color(0xfff6b835)));
+ expect(palette.lightVibrantColor!.color,
+ within<Color>(distance: 8, from: const Color(0xff82b2e9)));
+ expect(palette.darkVibrantColor!.color,
+ within<Color>(distance: 8, from: const Color(0xff3f630c)));
+ expect(palette.mutedColor!.color,
+ within<Color>(distance: 8, from: const Color(0xff6c7fa2)));
+ expect(palette.lightMutedColor!.color,
+ within<Color>(distance: 8, from: const Color(0xffc4b2b2)));
+ expect(palette.darkMutedColor!.color,
+ within<Color>(distance: 8, from: const Color(0xff3c4b2a)));
+ expect(palette.colors, containsAllInOrder(expectedColors));
+ expect(palette.colors.length, equals(palette.paletteColors.length));
+ });
+
+ test('PaletteGenerator limits max colors', () async {
+ final ImageProvider imageProvider = testImages['landscape']!;
+ PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
+ imageProvider,
+ maximumColorCount: 32);
+ expect(palette.paletteColors.length, equals(31));
+ palette = await PaletteGenerator.fromImageProvider(imageProvider,
+ maximumColorCount: 1);
+ expect(palette.paletteColors.length, equals(1));
+ palette = await PaletteGenerator.fromImageProvider(imageProvider,
+ maximumColorCount: 15);
+ expect(palette.paletteColors.length, equals(15));
+ });
+
+ test('PaletteGenerator Filters work', () async {
+ final ImageProvider imageProvider = testImages['landscape']!;
+ // First, test that supplying the default filter is the same as not supplying one.
+ List<PaletteFilter> filters = <PaletteFilter>[
+ avoidRedBlackWhitePaletteFilter
+ ];
+ PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
+ imageProvider,
+ filters: filters);
+ final List<PaletteColor> expectedSwatches = <PaletteColor>[
+ PaletteColor(const Color(0xff3f630c), 10137),
+ PaletteColor(const Color(0xff3c4b2a), 4773),
+ PaletteColor(const Color(0xff81b2e9), 4762),
+ PaletteColor(const Color(0xffc0d6ec), 4714),
+ PaletteColor(const Color(0xff4c4f50), 2465),
+ PaletteColor(const Color(0xff5c635b), 2463),
+ PaletteColor(const Color(0xff6e80a2), 2421),
+ PaletteColor(const Color(0xff9995a3), 1214),
+ PaletteColor(const Color(0xff676c4d), 1213),
+ PaletteColor(const Color(0xffc4b2b2), 1173),
+ PaletteColor(const Color(0xff445166), 1040),
+ PaletteColor(const Color(0xff475d83), 1019),
+ PaletteColor(const Color(0xff7e7360), 589),
+ PaletteColor(const Color(0xfff6b835), 286),
+ PaletteColor(const Color(0xffb9983d), 152),
+ PaletteColor(const Color(0xffe3ab35), 149),
+ ];
+ final Iterable<Color> expectedColors =
+ expectedSwatches.map<Color>((PaletteColor swatch) => swatch.color);
+ expect(palette.paletteColors, containsAll(expectedSwatches));
+ expect(palette.dominantColor, isNotNull);
+ expect(palette.dominantColor!.color,
+ within<Color>(distance: 8, from: const Color(0xff3f630c)));
+ expect(palette.colors, containsAllInOrder(expectedColors));
+
+ // A non-default filter works (and the default filter isn't applied too).
+ filters = <PaletteFilter>[onlyBluePaletteFilter];
+ palette = await PaletteGenerator.fromImageProvider(imageProvider,
+ filters: filters);
+ final List<PaletteColor> blueSwatches = <PaletteColor>[
+ PaletteColor(const Color(0xff4c5c75), 1515),
+ PaletteColor(const Color(0xff7483a1), 1505),
+ PaletteColor(const Color(0xff515661), 1476),
+ PaletteColor(const Color(0xff769dd4), 1470),
+ PaletteColor(const Color(0xff3e4858), 777),
+ PaletteColor(const Color(0xff98a3bc), 760),
+ PaletteColor(const Color(0xffb4c7e0), 760),
+ PaletteColor(const Color(0xff99bbe5), 742),
+ PaletteColor(const Color(0xffcbdef0), 701),
+ PaletteColor(const Color(0xff1c212b), 429),
+ PaletteColor(const Color(0xff393c46), 417),
+ PaletteColor(const Color(0xff526483), 394),
+ PaletteColor(const Color(0xff61708b), 372),
+ PaletteColor(const Color(0xff5e8ccc), 345),
+ PaletteColor(const Color(0xff587ab4), 194),
+ PaletteColor(const Color(0xff5584c8), 182),
+ ];
+ final Iterable<Color> expectedBlues =
+ blueSwatches.map<Color>((PaletteColor swatch) => swatch.color);
+
+ expect(palette.paletteColors, containsAll(blueSwatches));
+ expect(palette.dominantColor, isNotNull);
+ expect(palette.dominantColor!.color,
+ within<Color>(distance: 8, from: const Color(0xff4c5c75)));
+ expect(palette.colors, containsAllInOrder(expectedBlues));
+
+ // More than one filter is the intersection of the two filters.
+ filters = <PaletteFilter>[onlyBluePaletteFilter, onlyCyanPaletteFilter];
+ palette = await PaletteGenerator.fromImageProvider(imageProvider,
+ filters: filters);
+ final List<PaletteColor> blueGreenSwatches = <PaletteColor>[
+ PaletteColor(const Color(0xffc8e8f8), 87),
+ PaletteColor(const Color(0xff5c6c74), 73),
+ PaletteColor(const Color(0xff6f8088), 49),
+ PaletteColor(const Color(0xff687880), 49),
+ PaletteColor(const Color(0xff506068), 45),
+ PaletteColor(const Color(0xff485860), 39),
+ PaletteColor(const Color(0xff405058), 21),
+ PaletteColor(const Color(0xffd6ebf3), 11),
+ PaletteColor(const Color(0xff2f3f47), 7),
+ PaletteColor(const Color(0xff0f1f27), 6),
+ PaletteColor(const Color(0xffc0e0f0), 6),
+ PaletteColor(const Color(0xff203038), 3),
+ PaletteColor(const Color(0xff788890), 2),
+ PaletteColor(const Color(0xff384850), 2),
+ PaletteColor(const Color(0xff98a8b0), 1),
+ PaletteColor(const Color(0xffa8b8c0), 1),
+ ];
+ final Iterable<Color> expectedBlueGreens =
+ blueGreenSwatches.map<Color>((PaletteColor swatch) => swatch.color);
+
+ expect(palette.paletteColors, containsAll(blueGreenSwatches));
+ expect(palette.dominantColor, isNotNull);
+ expect(palette.dominantColor!.color,
+ within<Color>(distance: 8, from: const Color(0xffc8e8f8)));
+ expect(palette.colors, containsAllInOrder(expectedBlueGreens));
+
+ // Mutually exclusive filters return an empty palette.
+ filters = <PaletteFilter>[onlyBluePaletteFilter, onlyGreenPaletteFilter];
+ palette = await PaletteGenerator.fromImageProvider(imageProvider,
+ filters: filters);
+ expect(palette.paletteColors, isEmpty);
+ expect(palette.dominantColor, isNull);
+ expect(palette.colors, isEmpty);
+ });
+
+ test('PaletteGenerator targets work', () async {
+ final ImageProvider imageProvider = testImages['landscape']!;
+ // Passing an empty set of targets works the same as passing a null targets
+ // list.
+ PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
+ imageProvider,
+ targets: <PaletteTarget>[]);
+ expect(palette.selectedSwatches, isNotEmpty);
+ expect(palette.vibrantColor, isNotNull);
+ expect(palette.lightVibrantColor, isNotNull);
+ expect(palette.darkVibrantColor, isNotNull);
+ expect(palette.mutedColor, isNotNull);
+ expect(palette.lightMutedColor, isNotNull);
+ expect(palette.darkMutedColor, isNotNull);
+
+ // Passing targets augments the baseTargets, and those targets are found.
+ final List<PaletteTarget> saturationExtremeTargets = <PaletteTarget>[
+ PaletteTarget(minimumSaturation: 0.85),
+ PaletteTarget(maximumSaturation: .25),
+ ];
+ palette = await PaletteGenerator.fromImageProvider(imageProvider,
+ targets: saturationExtremeTargets);
+ expect(palette.vibrantColor, isNotNull);
+ expect(palette.lightVibrantColor, isNotNull);
+ expect(palette.darkVibrantColor, isNotNull);
+ expect(palette.mutedColor, isNotNull);
+ expect(palette.lightMutedColor, isNotNull);
+ expect(palette.darkMutedColor, isNotNull);
+ expect(palette.selectedSwatches.length,
+ equals(PaletteTarget.baseTargets.length + 2));
+ final PaletteColor? selectedSwatchesFirst =
+ palette.selectedSwatches[saturationExtremeTargets[0]];
+ final PaletteColor? selectedSwatchesSecond =
+ palette.selectedSwatches[saturationExtremeTargets[1]];
+ expect(selectedSwatchesFirst, isNotNull);
+ expect(selectedSwatchesSecond, isNotNull);
+ expect(selectedSwatchesFirst!.color, equals(const Color(0xfff6b835)));
+ expect(selectedSwatchesSecond!.color, equals(const Color(0xff6e80a2)));
+ });
+
+ test('PaletteGenerator produces consistent results', () async {
+ final ImageProvider imageProvider = testImages['landscape']!;
+
+ PaletteGenerator lastPalette =
+ await PaletteGenerator.fromImageProvider(imageProvider);
+ for (int i = 0; i < 5; ++i) {
+ final PaletteGenerator palette =
+ await PaletteGenerator.fromImageProvider(imageProvider);
+ expect(palette.paletteColors.length, lastPalette.paletteColors.length);
+ expect(palette.vibrantColor, equals(lastPalette.vibrantColor));
+ expect(palette.lightVibrantColor, equals(lastPalette.lightVibrantColor));
+ expect(palette.darkVibrantColor, equals(lastPalette.darkVibrantColor));
+ expect(palette.mutedColor, equals(lastPalette.mutedColor));
+ expect(palette.lightMutedColor, equals(lastPalette.lightMutedColor));
+ expect(palette.darkMutedColor, equals(lastPalette.darkMutedColor));
+ expect(palette.dominantColor, isNotNull);
+ expect(lastPalette.dominantColor, isNotNull);
+ expect(palette.dominantColor!.color,
+ within<Color>(distance: 8, from: lastPalette.dominantColor!.color));
+ lastPalette = palette;
+ }
+ });
+}
+
+bool onlyBluePaletteFilter(HSLColor hslColor) {
+ const double blueLineMinHue = 185.0;
+ const double blueLineMaxHue = 260.0;
+ const double blueLineMaxSaturation = 0.82;
+ return hslColor.hue >= blueLineMinHue &&
+ hslColor.hue <= blueLineMaxHue &&
+ hslColor.saturation <= blueLineMaxSaturation;
+}
+
+bool onlyCyanPaletteFilter(HSLColor hslColor) {
+ const double cyanLineMinHue = 165.0;
+ const double cyanLineMaxHue = 200.0;
+ const double cyanLineMaxSaturation = 0.82;
+ return hslColor.hue >= cyanLineMinHue &&
+ hslColor.hue <= cyanLineMaxHue &&
+ hslColor.saturation <= cyanLineMaxSaturation;
+}
+
+bool onlyGreenPaletteFilter(HSLColor hslColor) {
+ const double greenLineMinHue = 80.0;
+ const double greenLineMaxHue = 165.0;
+ const double greenLineMaxSaturation = 0.82;
+ return hslColor.hue >= greenLineMinHue &&
+ hslColor.hue <= greenLineMaxHue &&
+ hslColor.saturation <= greenLineMaxSaturation;
+}
diff --git a/packages/pigeon/.gitignore b/packages/pigeon/.gitignore
new file mode 100644
index 0000000..a70e872
--- /dev/null
+++ b/packages/pigeon/.gitignore
@@ -0,0 +1,15 @@
+build/
+e2e_tests/test_objc/ios/Flutter/
+platform_tests/ios_unit_tests/ios/Runner/messages.h
+platform_tests/ios_unit_tests/ios/Runner/messages.m
+xcuserdata/
+.gradle/
+.flutter-plugins
+.flutter-plugins-dependencies
+*.iml
+**/.symlinks/
+gradlew
+gradlew.bat
+local.properties
+gradle-wrapper.jar
+
diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
new file mode 100644
index 0000000..0125754
--- /dev/null
+++ b/packages/pigeon/CHANGELOG.md
@@ -0,0 +1,210 @@
+## 0.2.1
+
+* Java: Fixed issue where multiple async HostApis can generate multiple Result interfaces.
+* Dart: Made it so you can specify the BinaryMessenger of the generated APIs.
+
+## 0.2.0
+
+* **BREAKING CHANGE** - Pigeon files must be null-safe now. That means the
+ fields inside of the classes must be declared nullable (
+ [non-null fields](https://github.com/flutter/flutter/issues/59118) arent't yet
+ supported). Migration example:
+
+```dart
+// Version 0.1.x
+class Foo {
+ int bar;
+ String baz;
+}
+
+// Version 0.2.x
+class Foo {
+ int? bar;
+ String? baz;
+}
+```
+
+* **BREAKING CHANGE** - The default output from Pigeon is now null-safe. If you
+ want non-null-safe code you must provide the `--no-dart_null_safety` flag.
+* The Pigeon source code is now null-safe.
+* Fixed niladic non-value returning async functions in the Java generator.
+* Made `runCommandLine` return an the status code.
+
+## 0.1.24
+
+* Moved logic from bin/ to lib/ to help customers wrap up the behavior.
+* Added some more linter ignores for Dart.
+
+## 0.1.23
+
+* More Java linter and linter fixes.
+
+## 0.1.22
+
+* Java code generator enhancements:
+ * Added linter tests to CI.
+ * Fixed some linter issues in the Java code.
+
+## 0.1.21
+
+* Fixed decode method on generated Flutter classes that use null-safety and have
+ null values.
+
+## 0.1.20
+
+* Implemented `@async` HostApi's for iOS.
+* Fixed async FlutterApi methods with void return.
+
+## 0.1.19
+
+* Fixed a bug introduced in 0.1.17 where methods without arguments were
+ no longer being called.
+
+## 0.1.18
+
+* Null safe requires Dart 2.12.
+
+## 0.1.17
+
+* Split out test code generation for Dart into a separate file via the
+ --dart_test_out flag.
+
+## 0.1.16
+
+* Fixed running in certain environments where NNBD is enabled by default.
+
+## 0.1.15
+
+* Added support for running in versions of Dart that support NNBD.
+
+## 0.1.14
+
+* [Windows] Fixed executing from drives other than C:.
+
+## 0.1.13
+
+* Fixed execution on Windows with certain setups where Dart didn't allow
+ backslashes in `import` statements.
+
+## 0.1.12
+
+* Fixed assert failure with creating a PlatformException as a result of an
+ exception in Java handlers.
+
+## 0.1.11
+
+* Added flag to generate null safety annotated Dart code `--dart_null_safety`.
+* Made it so Dart API setup methods can take null.
+
+## 0.1.10+1
+
+* Updated the examples page.
+
+## 0.1.10
+
+* Fixed bug that prevented running `pigeon` on Windows (introduced in `0.1.8`).
+
+## 0.1.9
+
+* Fixed bug where executing pigeon without arguments would crash (introduced in 0.1.8).
+
+## 0.1.8
+
+* Started spawning pigeon_lib in an isolate instead of a subprocess. The
+ subprocess could have lead to errors if the dart version on $PATH didn't match
+ the one that comes with flutter.
+
+## 0.1.7
+
+* Fixed Dart compilation for later versions that support null safety, opting out
+ of it for now.
+* Fixed nested types in the Java runtime.
+
+## 0.1.6
+
+* Fixed unused variable linter warning in Dart code under certain conditions.
+
+## 0.1.5
+
+* Made array datatypes correctly get imported and exported avoiding the need to
+ add extra imports to generated code.
+
+## 0.1.4
+
+* Fixed nullability for NSError's in generated objc code.
+* Fixed nullability of nested objects in the Dart generator.
+* Added test to make sure the pigeon version is correct in generated code headers.
+
+## 0.1.3
+
+* Added error message if supported datatypes are used as arguments or return
+ types directly, without an enclosing class.
+* Added support for List and Map datatypes in Java and Objective-C targets.
+
+## 0.1.2+1
+
+* Updated the Readme.md.
+
+## 0.1.2
+
+* Removed static analysis warnings from generated Java code.
+
+## 0.1.1
+
+* Fixed issue where nested types didn't work if they weren't present in the Api.
+
+## 0.1.0
+
+* Added pigeon.dart.
+* Fixed some Obj-C linter problems.
+* Added the ability to generate a mock handler in Dart.
+
+## 0.1.0-experimental.11
+
+* Fixed setting an api to null in Java.
+
+## 0.1.0-experimental.10
+
+* Added support for void argument functions.
+* Added nullability annotations to generated objc code.
+
+## 0.1.0-experimental.9
+
+* Added e2e tests for iOS.
+
+## 0.1.0-experimental.8
+
+* Renamed `setupPigeon` to `configurePigeon`.
+
+## 0.1.0-experimental.7
+
+* Suppressed or got rid of warnings in generated Dart code.
+
+## 0.1.0-experimental.6
+
+* Added support for void return types.
+
+## 0.1.0-experimental.5
+
+* Fixed runtime exception in Android with values of ints less than 2^32.
+* Incremented codegen version warning.
+
+## 0.1.0-experimental.4
+
+* Fixed primitive types for Android Java.
+
+## 0.1.0-experimental.3
+
+* Added support for for Android Java.
+
+## 0.1.0-experimental.2
+
+* Added Host->Flutter calls for Objective-C
+
+## 0.1.0-experimental.1
+
+* Fixed warning in the README.md
+
+## 0.1.0-experimental.0
+
+* Initial release.
diff --git a/packages/pigeon/CONTRIBUTING.md b/packages/pigeon/CONTRIBUTING.md
new file mode 100644
index 0000000..d4174c8
--- /dev/null
+++ b/packages/pigeon/CONTRIBUTING.md
@@ -0,0 +1,74 @@
+# Pigeon Contributor's Guide
+
+## Description
+
+Pigeon is a code generation tool that adds type safety to Flutter’s Platform
+Channels. This document serves as an overview of how it functions to help
+people who would like to contribute to the project.
+
+## State Diagram
+
+Pigeon generates a temporary file in its _LaunchIsolate_, the isolate that is
+spawned to run `main()`, then launches another isolate, _PigeonIsolate_, that
+uses `dart:mirrors` to parse the generated file, creating an
+[AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree), then running code
+generators with that AST.
+
+
+
+## Source Index
+
+* [ast.dart](./lib/ast.dart) - The data structure for representing the Abstract Syntax Tree.
+* [dart_generator.dart](./lib/dart_generator.dart) - The Dart code generator.
+* [java_generator.dart](./lib/java_generator.dart) - The Java code generator.
+* [objc_generator.dart](./lib/objc_generator.dart) - The Objective-C code
+ generator (header and source files).
+* [generator_tools.dart](./lib/generator_tools.dart) - Shared code between generators.
+* [pigeon_cl.dart](./lib/pigeon_cl.dart) - The top-level function executed by
+ the command line tool in [bin/][./bin].
+* [pigeon_lib.dart](./lib/pigeon_lib.dart) - The top-level function for the
+ PigeonIsolate and the AST generation code.
+* [pigeon.dart](./lib/pigeon.dart) - A file of exported modules, the intended
+ import for users of Pigeon.
+
+## Testing Overview
+
+Pigeon has 3 types of tests, you'll find them all in [run_tests.sh](./run_tests.sh).
+
+* Unit tests - These are the fastest tests that are just typical unit tests,
+ they may be generating code and checking it against a regular expression to
+ see if it's correct. Example:
+ [dart_generator_test.dart](./test/dart_generator_test.dart)
+* Compilation tests - These tests generate code, then attempt to compile that
+ code. These are tests are much slower than unit tests, but not as slow as
+ integration tests. These tests are typically run against the Pigeon files in
+ [pigeons](./pigeons).
+* Integration tests - These tests generate code, then compile the generated
+ code, then execute the generated code. It can be thought of as unit-tests run
+ against the generated code. Examples: [platform_tests](./platform_tests)
+
+## Generated Source Code Example
+
+This is what the temporary generated code that the _PigeonIsolate_ executes
+looks like (see [State Diagram](#state-diagram)):
+
+```dart
+@dart = 2.12
+import 'path/to/supplied/pigeon/file.dart'
+import 'dart:io';
+import 'dart:isolate';
+import 'package:pigeon/pigeon_lib.dart';
+void main(List<String> args, SendPort sendPort) async {
+ sendPort.send(await Pigeon.run(args));
+}
+```
+
+This is how `dart:mirrors` gets access to the supplied Pigeon file.
+
+## Imminent Plans
+
+* Migrate to Dart Analyzer for AST generation ([issue
+ 78818](https://github.com/flutter/flutter/issues/78818)) - We might have
+ reached the limitations of using dart:mirrors for parsing the Dart files.
+ That package has been deprecated and it doesn't support null-safe annotations.
+ We should migrate to using the Dart Analyzer as the front-end parser.
diff --git a/packages/pigeon/LICENSE b/packages/pigeon/LICENSE
new file mode 100644
index 0000000..bc67b8f
--- /dev/null
+++ b/packages/pigeon/LICENSE
@@ -0,0 +1,27 @@
+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
diff --git a/packages/pigeon/README.md b/packages/pigeon/README.md
new file mode 100644
index 0000000..5283dc0
--- /dev/null
+++ b/packages/pigeon/README.md
@@ -0,0 +1,127 @@
+# Pigeon
+
+**Warning: Pigeon is prerelease, breaking changes may occur.**
+
+Pigeon is a code generator tool to make communication between Flutter and the
+host platform type-safe and easier.
+
+## Supported Platforms
+
+Currently Pigeon only supports generating Objective-C code for usage on iOS and
+Java code for Android. The Objective-C code is
+[accessible to Swift](https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/importing_objective-c_into_swift)
+and the Java code is accessible to Kotlin.
+
+## Runtime Requirements
+
+Pigeon generates all the code that is needed to communicate between Flutter and
+the host platform, there is no extra runtime requirement. A plugin author
+doesn't need to worry about conflicting versions of Pigeon.
+
+## Usage
+
+### Flutter calling into iOS Steps
+
+1) Add Pigeon as a dev_dependency.
+1) Make a ".dart" file outside of your "lib" directory for defining the communication interface.
+1) Run pigeon on your ".dart" file to generate the required Dart and Objective-C code:
+ `flutter pub get` then `flutter pub run pigeon` with suitable arguments.
+1) Add the generated Dart code to `lib` for compilation.
+1) Add the generated Objective-C code to your Xcode project for compilation
+ (e.g. `ios/Runner.xcworkspace` or `.podspec`).
+1) Implement the generated iOS protocol for handling the calls on iOS, set it up
+ as the handler for the messages.
+1) Call the generated Dart methods.
+
+### Flutter calling into Android Steps
+
+1) Add Pigeon as a dev_dependency.
+1) Make a ".dart" file outside of your "lib" directory for defining the communication interface.
+1) Run pigeon on your ".dart" file to generate the required Dart and Java code.
+ `flutter pub get` then `flutter pub run pigeon` with suitable arguments.
+1) Add the generated Dart code to `./lib` for compilation.
+1) Add the generated Java code to your `./android/app/src/main/java` directory for compilation.
+1) Implement the generated Java interface for handling the calls on Android, set it up
+ as the handler for the messages.
+1) Call the generated Dart methods.
+
+### Rules for defining your communication interface
+
+1) The file should contain no method or function definitions, only declarations.
+1) Datatypes are defined as classes with fields of the supported datatypes (see
+ the supported Datatypes section).
+1) Api's should be defined as an `abstract class` with either `HostApi()` or
+ `FlutterApi()` as metadata. The former being for procedures that are defined
+ on the host platform and the latter for procedures that are defined in Dart.
+1) Method declarations on the Api classes should have one argument and a return
+ value whose types are defined in the file or be `void`.
+
+## Example
+
+See the "Example" tab. A full working example can also be found in the
+[video_player plugin](https://github.com/flutter/plugins/tree/master/packages/video_player).
+
+## Supported Datatypes
+
+Pigeon uses the `StandardMessageCodec` so it supports any data-type platform
+channels supports
+[[documentation](https://flutter.dev/docs/development/platform-integration/platform-channels#codec)].
+Nested data-types are supported, too.
+
+Note: Generics for List and Map aren't supported yet.
+
+## Asynchronous Handlers
+
+By default Pigeon will generate synchronous handlers for messages. If you want
+to be able to respond to a message asynchronously you can use the `@async`
+annotation as of version 0.1.20.
+
+Example:
+
+```dart
+class Value {
+ int? number;
+}
+
+@HostApi()
+abstract class Api2Host {
+ @async
+ Value calculate(Value value);
+}
+```
+
+Generates:
+
+```objc
+// Objc
+@protocol Api2Host
+-(void)calculate:(nullable Value *)input
+ completion:(void(^)(Value *_Nullable, FlutterError *_Nullable))completion;
+@end
+```
+
+```java
+// Java
+public interface Result<T> {
+ void success(T result);
+}
+
+/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/
+public interface Api2Host {
+ void calculate(Value arg, Result<Value> result);
+}
+```
+
+## Null Safety (NNBD)
+
+Right now Pigeon supports generating null-safe code, but it doesn't yet support
+[non-null fields](https://github.com/flutter/flutter/issues/59118).
+
+The default is to generate null-safe code but in order to generate non-null-safe
+code run Pigeon with the extra argument `--no-dart_null_safety`. For example:
+`flutter pub run pigeon --input ./pigeons/messages.dart --no-dart_null_safety --dart_out stdout`.
+
+## Feedback
+
+File an issue in [flutter/flutter](https://github.com/flutter/flutter) with the
+word 'pigeon' clearly in the title and cc **@gaaclarke**.
diff --git a/packages/pigeon/bin/pigeon.dart b/packages/pigeon/bin/pigeon.dart
new file mode 100644
index 0000000..3566bad
--- /dev/null
+++ b/packages/pigeon/bin/pigeon.dart
@@ -0,0 +1,13 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// @dart = 2.2
+
+import 'dart:io' show exit;
+
+import 'package:pigeon/pigeon_cl.dart';
+
+Future<void> main(List<String> args) async {
+ exit(await runCommandLine(args));
+}
diff --git a/packages/pigeon/ci/.gitignore b/packages/pigeon/ci/.gitignore
new file mode 100644
index 0000000..72e8ffc
--- /dev/null
+++ b/packages/pigeon/ci/.gitignore
@@ -0,0 +1 @@
+*
diff --git a/packages/pigeon/docs/pigeon_state.png b/packages/pigeon/docs/pigeon_state.png
new file mode 100644
index 0000000..79539ad
--- /dev/null
+++ b/packages/pigeon/docs/pigeon_state.png
Binary files differ
diff --git a/packages/pigeon/docs/pigeon_state.puml b/packages/pigeon/docs/pigeon_state.puml
new file mode 100644
index 0000000..4027ac4
--- /dev/null
+++ b/packages/pigeon/docs/pigeon_state.puml
@@ -0,0 +1,60 @@
+@startuml
+
+[*] -> LaunchIsolate
+
+state LaunchIsolate {
+ [*] --> ParseCommandLineArguments
+ ParseCommandLineArguments --> WriteTemporarySourceCode
+ WriteTemporarySourceCode --> SpawnPigeonIsolate
+ SpawnPigeonIsolate --> WaitForPigeonIsolate
+ WaitForPigeonIsolate --> [*]
+}
+
+LaunchIsolate -> [*]
+
+state PigeonIsolate {
+ [*] --> ParseCommandLineArguments2
+ ParseCommandLineArguments2 --> PrintUsage
+ PrintUsage --> [*]
+ ParseCommandLineArguments2 --> ExecuteConfigurePigeon
+ ExecuteConfigurePigeon --> GenerateAST
+ GenerateAST --> RunGenerators
+ RunGenerators --> PrintErrors
+ PrintErrors --> ReturnStatusCode
+ ReturnStatusCode --> [*]
+
+ state GenerateAST {
+ [*] --> CollectAnnotatedClasses
+ CollectAnnotatedClasses --> CollectAnnotatedClassesDependencies
+ CollectAnnotatedClassesDependencies --> BuildAST
+ BuildAST --> [*]
+ }
+
+ state RunGenerators {
+ state DartTestGeneratorFork <<fork>>
+ state DartTestGeneratorJoin <<join>>
+ [*] --> DartTestGeneratorFork
+ DartTestGeneratorFork --> DartTestGeneratorJoin
+ DartTestGeneratorFork --> DartTestGenerator
+ DartTestGenerator --> DartTestGeneratorJoin
+ DartTestGeneratorJoin --> [*]
+ ||
+ state DartGeneratorFork <<fork>>
+ state DartGeneratorJoin <<join>>
+ [*] --> DartGeneratorFork
+ DartGeneratorFork --> DartGeneratorJoin
+ DartGeneratorFork --> DartGenerator
+ DartGenerator --> DartGeneratorJoin
+ DartGeneratorJoin --> [*]
+ ||
+ state JavaGeneratorFork <<fork>>
+ state JavaGeneratorJoin <<join>>
+ [*] --> JavaGeneratorFork
+ JavaGeneratorFork --> JavaGeneratorJoin
+ JavaGeneratorFork --> JavaGenerator
+ JavaGenerator --> JavaGeneratorJoin
+ JavaGeneratorJoin --> [*]
+ }
+}
+
+@enduml
\ No newline at end of file
diff --git a/packages/pigeon/e2e_tests/test_objc/README.md b/packages/pigeon/e2e_tests/test_objc/README.md
new file mode 100644
index 0000000..d9cf13e
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/README.md
@@ -0,0 +1,3 @@
+# test_objc
+
+The testbed app for E2E tests.
diff --git a/packages/pigeon/e2e_tests/test_objc/android/.project b/packages/pigeon/e2e_tests/test_objc/android/.project
new file mode 100644
index 0000000..3964dd3
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>android</name>
+ <comment>Project android created by Buildship.</comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.buildship.core.gradleprojectbuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
+ </natures>
+</projectDescription>
diff --git a/packages/pigeon/e2e_tests/test_objc/android/.settings/org.eclipse.buildship.core.prefs b/packages/pigeon/e2e_tests/test_objc/android/.settings/org.eclipse.buildship.core.prefs
new file mode 100644
index 0000000..861b6d8
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/.settings/org.eclipse.buildship.core.prefs
@@ -0,0 +1,13 @@
+arguments=
+auto.sync=false
+build.scans.enabled=false
+connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
+connection.project.dir=
+eclipse.preferences.version=1
+gradle.user.home=
+java.home=/Library/Java/JavaVirtualMachines/jdk-11.0.4.jdk/Contents/Home
+jvm.arguments=
+offline.mode=false
+override.workspace.settings=true
+show.console.view=true
+show.executions.view=true
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/.classpath b/packages/pigeon/e2e_tests/test_objc/android/app/.classpath
new file mode 100644
index 0000000..4a04201
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/.classpath
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/>
+ <classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
+ <classpathentry kind="output" path="bin/default"/>
+</classpath>
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/.project b/packages/pigeon/e2e_tests/test_objc/android/app/.project
new file mode 100644
index 0000000..ac485d7
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/.project
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>app</name>
+ <comment>Project app created by Buildship.</comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.buildship.core.gradleprojectbuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
+ </natures>
+</projectDescription>
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/.settings/org.eclipse.buildship.core.prefs b/packages/pigeon/e2e_tests/test_objc/android/app/.settings/org.eclipse.buildship.core.prefs
new file mode 100644
index 0000000..b1886ad
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/.settings/org.eclipse.buildship.core.prefs
@@ -0,0 +1,2 @@
+connection.project.dir=..
+eclipse.preferences.version=1
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/build.gradle b/packages/pigeon/e2e_tests/test_objc/android/app/build.gradle
new file mode 100644
index 0000000..ec6262c
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/build.gradle
@@ -0,0 +1,67 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+ throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply 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.test_objc"
+ 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/pigeon/e2e_tests/test_objc/android/app/src/debug/AndroidManifest.xml b/packages/pigeon/e2e_tests/test_objc/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..8172dab
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.test_objc">
+ <!-- Flutter needs it to communicate with the running application
+ to allow setting breakpoints, to provide hot reload, etc.
+ -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/AndroidManifest.xml b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b62dd45
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.test_objc">
+ <!-- io.flutter.app.FlutterApplication is an android.app.Application that
+ calls FlutterMain.startInitialization(this); in its onCreate method.
+ In most cases you can leave this as-is, but you if you want to provide
+ additional functionality it is fine to subclass or reimplement
+ FlutterApplication and put your custom class here. -->
+ <application
+ android:name="io.flutter.app.FlutterApplication"
+ android:label="test_objc"
+ android:icon="@mipmap/ic_launcher">
+ <activity
+ android:name=".MainActivity"
+ android:launchMode="singleTop"
+ android:theme="@style/LaunchTheme"
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+ android:hardwareAccelerated="true"
+ android:windowSoftInputMode="adjustResize">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <!-- Don't delete the meta-data below.
+ This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+ <meta-data
+ android:name="flutterEmbedding"
+ android:value="2" />
+ </application>
+</manifest>
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/java/io/flutter/plugins/Pigeon.java b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/java/io/flutter/plugins/Pigeon.java
new file mode 100644
index 0000000..a6962f7
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/java/io/flutter/plugins/Pigeon.java
@@ -0,0 +1,232 @@
+// Autogenerated from Pigeon (v0.1.2), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+package dev.flutter.aaclarke.pigeon;
+
+import io.flutter.plugin.common.BasicMessageChannel;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.StandardMessageCodec;
+import java.util.HashMap;
+
+/** Generated class from Pigeon. */
+@SuppressWarnings("unused")
+public class Pigeon {
+
+ /** Generated class from Pigeon that represents data sent in messages. */
+ public static class SearchReply {
+ private String result;
+
+ public String getResult() {
+ return result;
+ }
+
+ public void setResult(String setterArg) {
+ this.result = setterArg;
+ }
+
+ private String error;
+
+ public String getError() {
+ return error;
+ }
+
+ public void setError(String setterArg) {
+ this.error = setterArg;
+ }
+
+ HashMap toMap() {
+ HashMap<String, Object> toMapResult = new HashMap<>();
+ toMapResult.put("result", result);
+ toMapResult.put("error", error);
+ return toMapResult;
+ }
+
+ static SearchReply fromMap(HashMap map) {
+ SearchReply fromMapResult = new SearchReply();
+ Object result = map.get("result");
+ fromMapResult.result = (String) result;
+ Object error = map.get("error");
+ fromMapResult.error = (String) error;
+ return fromMapResult;
+ }
+ }
+
+ /** Generated class from Pigeon that represents data sent in messages. */
+ public static class SearchRequest {
+ private String query;
+
+ public String getQuery() {
+ return query;
+ }
+
+ public void setQuery(String setterArg) {
+ this.query = setterArg;
+ }
+
+ private Long anInt;
+
+ public Long getAnInt() {
+ return anInt;
+ }
+
+ public void setAnInt(Long setterArg) {
+ this.anInt = setterArg;
+ }
+
+ private Boolean aBool;
+
+ public Boolean getABool() {
+ return aBool;
+ }
+
+ public void setABool(Boolean setterArg) {
+ this.aBool = setterArg;
+ }
+
+ HashMap toMap() {
+ HashMap<String, Object> toMapResult = new HashMap<>();
+ toMapResult.put("query", query);
+ toMapResult.put("anInt", anInt);
+ toMapResult.put("aBool", aBool);
+ return toMapResult;
+ }
+
+ static SearchRequest fromMap(HashMap map) {
+ SearchRequest fromMapResult = new SearchRequest();
+ Object query = map.get("query");
+ fromMapResult.query = (String) query;
+ Object anInt = map.get("anInt");
+ fromMapResult.anInt =
+ (anInt == null) ? null : ((anInt instanceof Integer) ? (Integer) anInt : (Long) anInt);
+ Object aBool = map.get("aBool");
+ fromMapResult.aBool = (Boolean) aBool;
+ return fromMapResult;
+ }
+ }
+
+ /** Generated class from Pigeon that represents data sent in messages. */
+ public static class Nested {
+ private SearchRequest request;
+
+ public SearchRequest getRequest() {
+ return request;
+ }
+
+ public void setRequest(SearchRequest setterArg) {
+ this.request = setterArg;
+ }
+
+ HashMap toMap() {
+ HashMap<String, Object> toMapResult = new HashMap<>();
+ toMapResult.put("request", request);
+ return toMapResult;
+ }
+
+ static Nested fromMap(HashMap map) {
+ Nested fromMapResult = new Nested();
+ Object request = map.get("request");
+ fromMapResult.request = (SearchRequest) request;
+ return fromMapResult;
+ }
+ }
+
+ /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */
+ public static class FlutterSearchApi {
+ private final BinaryMessenger binaryMessenger;
+
+ public FlutterSearchApi(BinaryMessenger argBinaryMessenger) {
+ this.binaryMessenger = argBinaryMessenger;
+ }
+
+ public interface Reply<T> {
+ void reply(T reply);
+ }
+
+ public void search(SearchRequest argInput, Reply<SearchReply> callback) {
+ BasicMessageChannel<Object> channel =
+ new BasicMessageChannel<>(
+ binaryMessenger,
+ "dev.flutter.pigeon.FlutterSearchApi.search",
+ new StandardMessageCodec());
+ HashMap inputMap = argInput.toMap();
+ channel.send(
+ inputMap,
+ channelReply -> {
+ HashMap outputMap = (HashMap) channelReply;
+ @SuppressWarnings("ConstantConditions")
+ SearchReply output = SearchReply.fromMap(outputMap);
+ callback.reply(output);
+ });
+ }
+ }
+
+ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+ public interface NestedApi {
+ SearchReply search(Nested arg);
+
+ /** Sets up an instance of `NestedApi` to handle messages through the `binaryMessenger` */
+ static void setup(BinaryMessenger binaryMessenger, NestedApi api) {
+ {
+ BasicMessageChannel<Object> channel =
+ new BasicMessageChannel<>(
+ binaryMessenger, "dev.flutter.pigeon.NestedApi.search", new StandardMessageCodec());
+ if (api != null) {
+ channel.setMessageHandler(
+ (message, reply) -> {
+ HashMap<String, HashMap> wrapped = new HashMap<>();
+ try {
+ @SuppressWarnings("ConstantConditions")
+ Nested input = Nested.fromMap((HashMap) message);
+ SearchReply output = api.search(input);
+ wrapped.put("result", output.toMap());
+ } catch (Exception exception) {
+ wrapped.put("error", wrapError(exception));
+ }
+ reply.reply(wrapped);
+ });
+ } else {
+ channel.setMessageHandler(null);
+ }
+ }
+ }
+ }
+
+ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+ public interface Api {
+ SearchReply search(SearchRequest arg);
+
+ /** Sets up an instance of `Api` to handle messages through the `binaryMessenger` */
+ static void setup(BinaryMessenger binaryMessenger, Api api) {
+ {
+ BasicMessageChannel<Object> channel =
+ new BasicMessageChannel<>(
+ binaryMessenger, "dev.flutter.pigeon.Api.search", new StandardMessageCodec());
+ if (api != null) {
+ channel.setMessageHandler(
+ (message, reply) -> {
+ HashMap<String, HashMap> wrapped = new HashMap<>();
+ try {
+ @SuppressWarnings("ConstantConditions")
+ SearchRequest input = SearchRequest.fromMap((HashMap) message);
+ SearchReply output = api.search(input);
+ wrapped.put("result", output.toMap());
+ } catch (Exception exception) {
+ wrapped.put("error", wrapError(exception));
+ }
+ reply.reply(wrapped);
+ });
+ } else {
+ channel.setMessageHandler(null);
+ }
+ }
+ }
+ }
+
+ private static HashMap wrapError(Exception exception) {
+ HashMap<String, Object> errorMap = new HashMap<>();
+ errorMap.put("message", exception.toString());
+ errorMap.put("code", null);
+ errorMap.put("details", null);
+ return errorMap;
+ }
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/kotlin/com/example/test_objc/MainActivity.kt b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/kotlin/com/example/test_objc/MainActivity.kt
new file mode 100644
index 0000000..2907dd8
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/kotlin/com/example/test_objc/MainActivity.kt
@@ -0,0 +1,12 @@
+package com.example.test_objc
+
+import androidx.annotation.NonNull;
+import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugins.GeneratedPluginRegistrant
+
+class MainActivity: FlutterActivity() {
+ override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
+ GeneratedPluginRegistrant.registerWith(flutterEngine);
+ }
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/drawable/launch_background.xml b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@android:color/white" />
+
+ <!-- You can insert your own image assets here -->
+ <!-- <item>
+ <bitmap
+ android:gravity="center"
+ android:src="@mipmap/launch_image" />
+ </item> -->
+</layer-list>
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/values/styles.xml b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..00fa441
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+ <!-- Show a splash screen on the activity. Automatically removed when
+ Flutter draws its first frame -->
+ <item name="android:windowBackground">@drawable/launch_background</item>
+ </style>
+</resources>
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/profile/AndroidManifest.xml b/packages/pigeon/e2e_tests/test_objc/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..8172dab
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.test_objc">
+ <!-- Flutter needs it to communicate with the running application
+ to allow setting breakpoints, to provide hot reload, etc.
+ -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
diff --git a/packages/pigeon/e2e_tests/test_objc/android/build.gradle b/packages/pigeon/e2e_tests/test_objc/android/build.gradle
new file mode 100644
index 0000000..3100ad2
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/build.gradle
@@ -0,0 +1,31 @@
+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/pigeon/e2e_tests/test_objc/android/gradle.properties b/packages/pigeon/e2e_tests/test_objc/android/gradle.properties
new file mode 100644
index 0000000..38c8d45
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/pigeon/e2e_tests/test_objc/android/gradle/wrapper/gradle-wrapper.properties b/packages/pigeon/e2e_tests/test_objc/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..296b146
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
diff --git a/packages/pigeon/e2e_tests/test_objc/android/settings.gradle b/packages/pigeon/e2e_tests/test_objc/android/settings.gradle
new file mode 100644
index 0000000..5a2f14f
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/settings.gradle
@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+ pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+
+plugins.each { name, path ->
+ def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+ include ":$name"
+ project(":$name").projectDir = pluginDirectory
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Flutter/AppFrameworkInfo.plist b/packages/pigeon/e2e_tests/test_objc/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..6b4c0f7
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>App</string>
+ <key>CFBundleIdentifier</key>
+ <string>io.flutter.flutter.app</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>App</string>
+ <key>CFBundlePackageType</key>
+ <string>FMWK</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>MinimumOSVersion</key>
+ <string>8.0</string>
+</dict>
+</plist>
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Flutter/Debug.xcconfig b/packages/pigeon/e2e_tests/test_objc/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..e8efba1
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Flutter/Release.xcconfig b/packages/pigeon/e2e_tests/test_objc/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..399e934
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Podfile b/packages/pigeon/e2e_tests/test_objc/ios/Podfile
new file mode 100644
index 0000000..c382d62
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Podfile
@@ -0,0 +1,42 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '9.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end
+
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/project.pbxproj b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..d8ec407
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,743 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 0D89130E23C8F951005E0326 /* dartle.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D89130D23C8F951005E0326 /* dartle.m */; };
+ 0D89131123C8FB14005E0326 /* MyFlutterViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D89131023C8FB14005E0326 /* MyFlutterViewController.m */; };
+ 0D89131423C8FCD2005E0326 /* MyApi.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D89131323C8FCD2005E0326 /* MyApi.m */; };
+ 0DA878B02453583800622EF2 /* MyNestedApi.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DA878AF2453583800622EF2 /* MyNestedApi.m */; };
+ 0DD149D423C926D900ABB3D6 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DD149D323C926D900ABB3D6 /* RunnerTests.m */; };
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 5F4169D8C45B4C68F36D3EDA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C06A312A3EB22F18BB22C39C /* Pods_Runner.framework */; };
+ 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
+ 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 0DD149D623C926D900ABB3D6 /* 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 */
+ 0D89130C23C8F951005E0326 /* dartle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = dartle.h; sourceTree = "<group>"; };
+ 0D89130D23C8F951005E0326 /* dartle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = dartle.m; sourceTree = "<group>"; };
+ 0D89130F23C8FB14005E0326 /* MyFlutterViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MyFlutterViewController.h; sourceTree = "<group>"; };
+ 0D89131023C8FB14005E0326 /* MyFlutterViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MyFlutterViewController.m; sourceTree = "<group>"; };
+ 0D89131223C8FCD2005E0326 /* MyApi.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MyApi.h; sourceTree = "<group>"; };
+ 0D89131323C8FCD2005E0326 /* MyApi.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MyApi.m; sourceTree = "<group>"; };
+ 0DA878AE2453583800622EF2 /* MyNestedApi.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MyNestedApi.h; sourceTree = "<group>"; };
+ 0DA878AF2453583800622EF2 /* MyNestedApi.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MyNestedApi.m; sourceTree = "<group>"; };
+ 0DD149D123C926D900ABB3D6 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 0DD149D323C926D900ABB3D6 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = "<group>"; };
+ 0DD149D523C926D900ABB3D6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
+ 29BF8B7A9D0A8441FBFB29F1 /* 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 = "<group>"; };
+ 3B0FDBA7A0766A60FD7C1D91 /* 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 = "<group>"; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+ 3DE0496B7DCC7F565F839974 /* 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 = "<group>"; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
+ 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
+ 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
+ 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 = "<group>"; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ C06A312A3EB22F18BB22C39C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 0DD149CE23C926D900ABB3D6 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 5F4169D8C45B4C68F36D3EDA /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 0DD149D223C926D900ABB3D6 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 0DD149D323C926D900ABB3D6 /* RunnerTests.m */,
+ 0DD149D523C926D900ABB3D6 /* Info.plist */,
+ );
+ path = RunnerTests;
+ sourceTree = "<group>";
+ };
+ 508082A21EE4605616986B5E /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ C06A312A3EB22F18BB22C39C /* Pods_Runner.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+ 5ECC4FFF4A77B35E781D0484 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 3DE0496B7DCC7F565F839974 /* Pods-Runner.debug.xcconfig */,
+ 3B0FDBA7A0766A60FD7C1D91 /* Pods-Runner.release.xcconfig */,
+ 29BF8B7A9D0A8441FBFB29F1 /* Pods-Runner.profile.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "<group>";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "<group>";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 0DD149D223C926D900ABB3D6 /* RunnerTests */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 5ECC4FFF4A77B35E781D0484 /* Pods */,
+ 508082A21EE4605616986B5E /* Frameworks */,
+ );
+ sourceTree = "<group>";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ 0DD149D123C926D900ABB3D6 /* RunnerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 0D89130C23C8F951005E0326 /* dartle.h */,
+ 0D89130D23C8F951005E0326 /* dartle.m */,
+ 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 */,
+ 0D89130F23C8FB14005E0326 /* MyFlutterViewController.h */,
+ 0D89131023C8FB14005E0326 /* MyFlutterViewController.m */,
+ 0D89131223C8FCD2005E0326 /* MyApi.h */,
+ 0D89131323C8FCD2005E0326 /* MyApi.m */,
+ 0DA878AE2453583800622EF2 /* MyNestedApi.h */,
+ 0DA878AF2453583800622EF2 /* MyNestedApi.m */,
+ );
+ path = Runner;
+ sourceTree = "<group>";
+ };
+ 97C146F11CF9000F007C117D /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146F21CF9000F007C117D /* main.m */,
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 0DD149D023C926D900ABB3D6 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 0DD149DB23C926D900ABB3D6 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 0DD149CD23C926D900ABB3D6 /* Sources */,
+ 0DD149CE23C926D900ABB3D6 /* Frameworks */,
+ 0DD149CF23C926D900ABB3D6 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 0DD149D723C926D900ABB3D6 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 0DD149D123C926D900ABB3D6 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ C8AD4DD66A8AA54F9893603E /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ 2935EF385A5B690A2E773EB2 /* [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 = "";
+ TargetAttributes = {
+ 0DD149D023C926D900ABB3D6 = {
+ CreatedOnToolsVersion = 11.3;
+ 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 */,
+ 0DD149D023C926D900ABB3D6 /* RunnerTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 0DD149CF23C926D900ABB3D6 /* 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 */
+ 2935EF385A5B690A2E773EB2 /* [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\" 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";
+ };
+ C8AD4DD66A8AA54F9893603E /* [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 */
+ 0DD149CD23C926D900ABB3D6 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0DD149D423C926D900ABB3D6 /* RunnerTests.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0D89131423C8FCD2005E0326 /* MyApi.m in Sources */,
+ 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
+ 97C146F31CF9000F007C117D /* main.m in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ 0D89130E23C8F951005E0326 /* dartle.m in Sources */,
+ 0D89131123C8FB14005E0326 /* MyFlutterViewController.m in Sources */,
+ 0DA878B02453583800622EF2 /* MyNestedApi.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 0DD149D723C926D900ABB3D6 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 97C146ED1CF9000F007C117D /* Runner */;
+ targetProxy = 0DD149D623C926D900ABB3D6 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "<group>";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 0DD149D823C926D900ABB3D6 /* 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 = RunnerTests/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.aaclarke.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+ };
+ name = Debug;
+ };
+ 0DD149D923C926D900ABB3D6 /* 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 = RunnerTests/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.aaclarke.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+ };
+ name = Release;
+ };
+ 0DD149DA23C926D900ABB3D6 /* Profile */ = {
+ 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 = RunnerTests/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.aaclarke.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+ };
+ name = Profile;
+ };
+ 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 = 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.testObjc;
+ 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 = 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_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.testObjc;
+ 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.testObjc;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 0DD149DB23C926D900ABB3D6 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 0DD149D823C926D900ABB3D6 /* Debug */,
+ 0DD149D923C926D900ABB3D6 /* Release */,
+ 0DD149DA23C926D900ABB3D6 /* 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/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "group:Runner.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..595f1c6
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "1020"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "0DD149D023C926D900ABB3D6"
+ BuildableName = "RunnerTests.xctest"
+ BlueprintName = "RunnerTests"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Profile"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerTests.xcscheme b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerTests.xcscheme
new file mode 100644
index 0000000..b56e406
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerTests.xcscheme
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "1130"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "0DD149D023C926D900ABB3D6"
+ BuildableName = "RunnerTests.xctest"
+ BlueprintName = "RunnerTests"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..21a3cc1
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "group:Runner.xcodeproj">
+ </FileRef>
+ <FileRef
+ location = "group:Pods/Pods.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IDEDidComputeMac32BitWarning</key>
+ <true/>
+</dict>
+</plist>
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/AppDelegate.h b/packages/pigeon/e2e_tests/test_objc/ios/Runner/AppDelegate.h
new file mode 100644
index 0000000..36e21bb
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/AppDelegate.h
@@ -0,0 +1,6 @@
+#import <Flutter/Flutter.h>
+#import <UIKit/UIKit.h>
+
+@interface AppDelegate : FlutterAppDelegate
+
+@end
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/AppDelegate.m b/packages/pigeon/e2e_tests/test_objc/ios/Runner/AppDelegate.m
new file mode 100644
index 0000000..70e8393
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/AppDelegate.m
@@ -0,0 +1,13 @@
+#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/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..dc9ada4
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..28c6bf0
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..f091b6b
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cde121
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..d0ef06e
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..dcdc230
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..c8f9ed8
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..75b2d16
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..c4df70d
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..6a84f41
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..d0e1f58
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
+ <viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
+ </imageView>
+ </subviews>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ <constraints>
+ <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
+ <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
+ </constraints>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+ <resources>
+ <image name="LaunchImage" width="168" height="185"/>
+ </resources>
+</document>
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Base.lproj/Main.storyboard b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..c8b5b90
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
+ <device id="retina6_1" orientation="portrait" appearance="light"/>
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <scenes>
+ <!--My Flutter View Controller-->
+ <scene sceneID="tne-QT-ifu">
+ <objects>
+ <viewController id="BYZ-38-t0r" customClass="MyFlutterViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+ <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+ <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="25" y="29"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Info.plist b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Info.plist
new file mode 100644
index 0000000..58bf245
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>test_objc</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>$(FLUTTER_BUILD_NAME)</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>$(FLUTTER_BUILD_NUMBER)</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UIViewControllerBasedStatusBarAppearance</key>
+ <false/>
+</dict>
+</plist>
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyApi.h b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyApi.h
new file mode 100644
index 0000000..68940a9
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyApi.h
@@ -0,0 +1,10 @@
+#import <Foundation/Foundation.h>
+#import "dartle.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// Implementation of the Pigeon generated interface Api.
+@interface MyApi : NSObject<ACApi>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyApi.m b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyApi.m
new file mode 100644
index 0000000..8dccd43
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyApi.m
@@ -0,0 +1,15 @@
+#import "MyApi.h"
+#import <Flutter/Flutter.h>
+
+@implementation MyApi
+- (ACSearchReply*)search:(ACSearchRequest*)input error:(FlutterError**)error {
+ if ([input.query isEqualToString:@"error"]) {
+ *error = [FlutterError errorWithCode:@"somecode" message:@"somemessage" details:nil];
+ return nil;
+ } else {
+ ACSearchReply* reply = [[ACSearchReply alloc] init];
+ reply.result = [NSString stringWithFormat:@"Hello %@!", input.query];
+ return reply;
+ }
+}
+@end
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyFlutterViewController.h b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyFlutterViewController.h
new file mode 100644
index 0000000..43bf4ca
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyFlutterViewController.h
@@ -0,0 +1,8 @@
+#import <Flutter/Flutter.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MyFlutterViewController : FlutterViewController
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyFlutterViewController.m b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyFlutterViewController.m
new file mode 100644
index 0000000..5994306
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyFlutterViewController.m
@@ -0,0 +1,17 @@
+#import "MyFlutterViewController.h"
+#import "MyApi.h"
+#import "MyNestedApi.h"
+#import "dartle.h"
+
+@interface MyFlutterViewController ()
+@end
+
+@implementation MyFlutterViewController
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ ACApiSetup(self.engine.binaryMessenger, [[MyApi alloc] init]);
+ ACNestedApiSetup(self.engine.binaryMessenger, [[MyNestedApi alloc] init]);
+}
+
+@end
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyNestedApi.h b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyNestedApi.h
new file mode 100644
index 0000000..4dd1e26
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyNestedApi.h
@@ -0,0 +1,11 @@
+#import <Foundation/Foundation.h>
+#import "dartle.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// Implementation of the Pigeon generated interface NestedApi.
+@interface MyNestedApi : NSObject<ACNestedApi>
+- (ACSearchReply *)search:(ACNested *)input error:(FlutterError *_Nullable *_Nonnull)error;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyNestedApi.m b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyNestedApi.m
new file mode 100644
index 0000000..d563278
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyNestedApi.m
@@ -0,0 +1,9 @@
+#import "MyNestedApi.h"
+
+@implementation MyNestedApi
+- (ACSearchReply *)search:(ACNested *)input error:(FlutterError **)error {
+ ACSearchReply *reply = [[ACSearchReply alloc] init];
+ reply.result = [NSString stringWithFormat:@"Hello %@!", input.request.query];
+ return reply;
+}
+@end
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/dartle.h b/packages/pigeon/e2e_tests/test_objc/ios/Runner/dartle.h
new file mode 100644
index 0000000..3844745
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/dartle.h
@@ -0,0 +1,47 @@
+// Autogenerated from Pigeon (v0.1.2), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+#import <Foundation/Foundation.h>
+@protocol FlutterBinaryMessenger;
+@class FlutterError;
+@class FlutterStandardTypedData;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class ACSearchReply;
+@class ACSearchRequest;
+@class ACNested;
+
+@interface ACSearchReply : NSObject
+@property(nonatomic, copy, nullable) NSString *result;
+@property(nonatomic, copy, nullable) NSString *error;
+@end
+
+@interface ACSearchRequest : NSObject
+@property(nonatomic, copy, nullable) NSString *query;
+@property(nonatomic, strong, nullable) NSNumber *anInt;
+@property(nonatomic, strong, nullable) NSNumber *aBool;
+@end
+
+@interface ACNested : NSObject
+@property(nonatomic, strong, nullable) ACSearchRequest *request;
+@end
+
+@interface ACFlutterSearchApi : NSObject
+- (instancetype)initWithBinaryMessenger:(id<FlutterBinaryMessenger>)binaryMessenger;
+- (void)search:(ACSearchRequest *)input completion:(void (^)(ACSearchReply *, NSError *))completion;
+@end
+@protocol ACNestedApi
+- (nullable ACSearchReply *)search:(ACNested *)input error:(FlutterError *_Nullable *_Nonnull)error;
+@end
+
+extern void ACNestedApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
+ id<ACNestedApi> _Nullable api);
+
+@protocol ACApi
+- (nullable ACSearchReply *)search:(ACSearchRequest *)input
+ error:(FlutterError *_Nullable *_Nonnull)error;
+@end
+
+extern void ACApiSetup(id<FlutterBinaryMessenger> binaryMessenger, id<ACApi> _Nullable api);
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/dartle.m b/packages/pigeon/e2e_tests/test_objc/ios/Runner/dartle.m
new file mode 100644
index 0000000..7b52efa
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/dartle.m
@@ -0,0 +1,157 @@
+// Autogenerated from Pigeon (v0.1.2), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+#import "dartle.h"
+#import <Flutter/Flutter.h>
+
+#if !__has_feature(objc_arc)
+#error File requires ARC to be enabled.
+#endif
+
+static NSDictionary *wrapResult(NSDictionary *result, FlutterError *error) {
+ NSDictionary *errorDict = (NSDictionary *)[NSNull null];
+ if (error) {
+ errorDict = [NSDictionary
+ dictionaryWithObjectsAndKeys:(error.code ? error.code : [NSNull null]), @"code",
+ (error.message ? error.message : [NSNull null]), @"message",
+ (error.details ? error.details : [NSNull null]), @"details",
+ nil];
+ }
+ return [NSDictionary dictionaryWithObjectsAndKeys:(result ? result : [NSNull null]), @"result",
+ errorDict, @"error", nil];
+}
+
+@interface ACSearchReply ()
++ (ACSearchReply *)fromMap:(NSDictionary *)dict;
+- (NSDictionary *)toMap;
+@end
+@interface ACSearchRequest ()
++ (ACSearchRequest *)fromMap:(NSDictionary *)dict;
+- (NSDictionary *)toMap;
+@end
+@interface ACNested ()
++ (ACNested *)fromMap:(NSDictionary *)dict;
+- (NSDictionary *)toMap;
+@end
+
+@implementation ACSearchReply
++ (ACSearchReply *)fromMap:(NSDictionary *)dict {
+ ACSearchReply *result = [[ACSearchReply alloc] init];
+ result.result = dict[@"result"];
+ if ((NSNull *)result.result == [NSNull null]) {
+ result.result = nil;
+ }
+ result.error = dict[@"error"];
+ if ((NSNull *)result.error == [NSNull null]) {
+ result.error = nil;
+ }
+ return result;
+}
+- (NSDictionary *)toMap {
+ return [NSDictionary
+ dictionaryWithObjectsAndKeys:(self.result ? self.result : [NSNull null]), @"result",
+ (self.error ? self.error : [NSNull null]), @"error", nil];
+}
+@end
+
+@implementation ACSearchRequest
++ (ACSearchRequest *)fromMap:(NSDictionary *)dict {
+ ACSearchRequest *result = [[ACSearchRequest alloc] init];
+ result.query = dict[@"query"];
+ if ((NSNull *)result.query == [NSNull null]) {
+ result.query = nil;
+ }
+ result.anInt = dict[@"anInt"];
+ if ((NSNull *)result.anInt == [NSNull null]) {
+ result.anInt = nil;
+ }
+ result.aBool = dict[@"aBool"];
+ if ((NSNull *)result.aBool == [NSNull null]) {
+ result.aBool = nil;
+ }
+ return result;
+}
+- (NSDictionary *)toMap {
+ return [NSDictionary
+ dictionaryWithObjectsAndKeys:(self.query ? self.query : [NSNull null]), @"query",
+ (self.anInt ? self.anInt : [NSNull null]), @"anInt",
+ (self.aBool ? self.aBool : [NSNull null]), @"aBool", nil];
+}
+@end
+
+@implementation ACNested
++ (ACNested *)fromMap:(NSDictionary *)dict {
+ ACNested *result = [[ACNested alloc] init];
+ result.request = [ACSearchRequest fromMap:dict[@"request"]];
+ if ((NSNull *)result.request == [NSNull null]) {
+ result.request = nil;
+ }
+ return result;
+}
+- (NSDictionary *)toMap {
+ return [NSDictionary
+ dictionaryWithObjectsAndKeys:(self.request ? [self.request toMap] : [NSNull null]),
+ @"request", nil];
+}
+@end
+
+@interface ACFlutterSearchApi ()
+@property(nonatomic, strong) NSObject<FlutterBinaryMessenger> *binaryMessenger;
+@end
+
+@implementation ACFlutterSearchApi
+- (instancetype)initWithBinaryMessenger:(NSObject<FlutterBinaryMessenger> *)binaryMessenger {
+ self = [super init];
+ if (self) {
+ self.binaryMessenger = binaryMessenger;
+ }
+ return self;
+}
+
+- (void)search:(ACSearchRequest *)input
+ completion:(void (^)(ACSearchReply *, NSError *))completion {
+ FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
+ messageChannelWithName:@"dev.flutter.pigeon.FlutterSearchApi.search"
+ binaryMessenger:self.binaryMessenger];
+ NSDictionary *inputMap = [input toMap];
+ [channel sendMessage:inputMap
+ reply:^(id reply) {
+ NSDictionary *outputMap = reply;
+ ACSearchReply *output = [ACSearchReply fromMap:outputMap];
+ completion(output, nil);
+ }];
+}
+@end
+void ACNestedApiSetup(id<FlutterBinaryMessenger> binaryMessenger, id<ACNestedApi> api) {
+ {
+ FlutterBasicMessageChannel *channel =
+ [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.NestedApi.search"
+ binaryMessenger:binaryMessenger];
+ if (api) {
+ [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
+ FlutterError *error;
+ ACNested *input = [ACNested fromMap:message];
+ ACSearchReply *output = [api search:input error:&error];
+ callback(wrapResult([output toMap], error));
+ }];
+ } else {
+ [channel setMessageHandler:nil];
+ }
+ }
+}
+void ACApiSetup(id<FlutterBinaryMessenger> binaryMessenger, id<ACApi> api) {
+ {
+ FlutterBasicMessageChannel *channel =
+ [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.Api.search"
+ binaryMessenger:binaryMessenger];
+ if (api) {
+ [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
+ FlutterError *error;
+ ACSearchRequest *input = [ACSearchRequest fromMap:message];
+ ACSearchReply *output = [api search:input error:&error];
+ callback(wrapResult([output toMap], error));
+ }];
+ } else {
+ [channel setMessageHandler:nil];
+ }
+ }
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/main.m b/packages/pigeon/e2e_tests/test_objc/ios/Runner/main.m
new file mode 100644
index 0000000..dff6597
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/main.m
@@ -0,0 +1,9 @@
+#import <Flutter/Flutter.h>
+#import <UIKit/UIKit.h>
+#import "AppDelegate.h"
+
+int main(int argc, char* argv[]) {
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
+ }
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/RunnerTests/Info.plist b/packages/pigeon/e2e_tests/test_objc/ios/RunnerTests/Info.plist
new file mode 100644
index 0000000..64d65ca
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/RunnerTests/Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/RunnerTests/RunnerTests.m b/packages/pigeon/e2e_tests/test_objc/ios/RunnerTests/RunnerTests.m
new file mode 100644
index 0000000..9614c65
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/RunnerTests/RunnerTests.m
@@ -0,0 +1,4 @@
+#import <XCTest/XCTest.h>
+#import <e2e/E2EIosTest.h>
+
+E2E_IOS_RUNNER(RunnerTests)
diff --git a/packages/pigeon/e2e_tests/test_objc/lib/dartle.dart b/packages/pigeon/e2e_tests/test_objc/lib/dartle.dart
new file mode 100644
index 0000000..9f0db48
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/lib/dartle.dart
@@ -0,0 +1,147 @@
+// Autogenerated from Pigeon (v0.1.2), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import
+import 'dart:async';
+import 'package:flutter/services.dart';
+
+class SearchReply {
+ String result;
+ String error;
+ // ignore: unused_element
+ Map<dynamic, dynamic> _toMap() {
+ final Map<dynamic, dynamic> pigeonMap = <dynamic, dynamic>{};
+ pigeonMap['result'] = result;
+ pigeonMap['error'] = error;
+ return pigeonMap;
+ }
+
+ // ignore: unused_element
+ static SearchReply _fromMap(Map<dynamic, dynamic> pigeonMap) {
+ final SearchReply result = SearchReply();
+ result.result = pigeonMap['result'];
+ result.error = pigeonMap['error'];
+ return result;
+ }
+}
+
+class SearchRequest {
+ String query;
+ int anInt;
+ bool aBool;
+ // ignore: unused_element
+ Map<dynamic, dynamic> _toMap() {
+ final Map<dynamic, dynamic> pigeonMap = <dynamic, dynamic>{};
+ pigeonMap['query'] = query;
+ pigeonMap['anInt'] = anInt;
+ pigeonMap['aBool'] = aBool;
+ return pigeonMap;
+ }
+
+ // ignore: unused_element
+ static SearchRequest _fromMap(Map<dynamic, dynamic> pigeonMap) {
+ final SearchRequest result = SearchRequest();
+ result.query = pigeonMap['query'];
+ result.anInt = pigeonMap['anInt'];
+ result.aBool = pigeonMap['aBool'];
+ return result;
+ }
+}
+
+class Nested {
+ SearchRequest request;
+ // ignore: unused_element
+ Map<dynamic, dynamic> _toMap() {
+ final Map<dynamic, dynamic> pigeonMap = <dynamic, dynamic>{};
+ pigeonMap['request'] = request._toMap();
+ return pigeonMap;
+ }
+
+ // ignore: unused_element
+ static Nested _fromMap(Map<dynamic, dynamic> pigeonMap) {
+ final Nested result = Nested();
+ result.request = SearchRequest._fromMap(pigeonMap['request']);
+ return result;
+ }
+}
+
+abstract class FlutterSearchApi {
+ SearchReply search(SearchRequest arg);
+ static void setup(FlutterSearchApi api) {
+ {
+ const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>(
+ 'dev.flutter.pigeon.FlutterSearchApi.search', StandardMessageCodec());
+ channel.setMessageHandler((dynamic message) async {
+ final Map<dynamic, dynamic> mapMessage =
+ message as Map<dynamic, dynamic>;
+ final SearchRequest input = SearchRequest._fromMap(mapMessage);
+ final SearchReply output = api.search(input);
+ return output._toMap();
+ });
+ }
+ }
+}
+
+class NestedApi {
+ Future<SearchReply> search(Nested arg) async {
+ final Map<dynamic, dynamic> requestMap = arg._toMap();
+ const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>(
+ 'dev.flutter.pigeon.NestedApi.search', StandardMessageCodec());
+
+ final Map<dynamic, dynamic> replyMap = await channel.send(requestMap);
+ if (replyMap == null) {
+ throw PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel.',
+ details: null);
+ } else if (replyMap['error'] != null) {
+ final Map<dynamic, dynamic> error = replyMap['error'];
+ throw PlatformException(
+ code: error['code'],
+ message: error['message'],
+ details: error['details']);
+ } else {
+ return SearchReply._fromMap(replyMap['result']);
+ }
+ }
+}
+
+class Api {
+ Future<SearchReply> search(SearchRequest arg) async {
+ final Map<dynamic, dynamic> requestMap = arg._toMap();
+ const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>(
+ 'dev.flutter.pigeon.Api.search', StandardMessageCodec());
+
+ final Map<dynamic, dynamic> replyMap = await channel.send(requestMap);
+ if (replyMap == null) {
+ throw PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel.',
+ details: null);
+ } else if (replyMap['error'] != null) {
+ final Map<dynamic, dynamic> error = replyMap['error'];
+ throw PlatformException(
+ code: error['code'],
+ message: error['message'],
+ details: error['details']);
+ } else {
+ return SearchReply._fromMap(replyMap['result']);
+ }
+ }
+}
+
+abstract class TestHostApi {
+ SearchReply search(SearchRequest arg);
+ static void setup(TestHostApi api) {
+ {
+ const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>(
+ 'dev.flutter.pigeon.Api.search', StandardMessageCodec());
+ channel.setMockMessageHandler((dynamic message) async {
+ final Map<dynamic, dynamic> mapMessage =
+ message as Map<dynamic, dynamic>;
+ final SearchRequest input = SearchRequest._fromMap(mapMessage);
+ final SearchReply output = api.search(input);
+ return <dynamic, dynamic>{'result': output._toMap()};
+ });
+ }
+ }
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/lib/main.dart b/packages/pigeon/e2e_tests/test_objc/lib/main.dart
new file mode 100644
index 0000000..4e74743
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/lib/main.dart
@@ -0,0 +1,81 @@
+import 'package:flutter/material.dart';
+import 'dartle.dart';
+
+class _MyFlutterSearchApi extends FlutterSearchApi {
+ @override
+ SearchReply search(SearchRequest input) {
+ return SearchReply()..result = 'Hello ${input.query}, from Flutter';
+ }
+}
+
+void main() {
+ WidgetsFlutterBinding.ensureInitialized();
+ FlutterSearchApi.setup(_MyFlutterSearchApi());
+ runApp(const MyApp());
+}
+
+/// Main widget for the tests.
+class MyApp extends StatelessWidget {
+ /// Creates the main widget for the tests.
+ const MyApp({Key key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'Flutter Demo',
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
+ ),
+ home: const _MyHomePage(title: 'Flutter Demo Home Page'),
+ );
+ }
+}
+
+class _MyHomePage extends StatefulWidget {
+ const _MyHomePage({Key key, this.title}) : super(key: key);
+ final String title;
+
+ @override
+ _MyHomePageState createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State<_MyHomePage> {
+ String _message = '';
+
+ Future<void> _incrementCounter() async {
+ final SearchRequest request = SearchRequest()..query = 'Aaron';
+ final Api api = Api();
+ final SearchReply reply = await api.search(request);
+ setState(() {
+ _message = reply.result;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(widget.title),
+ ),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ const Text(
+ 'Message:',
+ ),
+ Text(
+ _message,
+ style: Theme.of(context).textTheme.headline1,
+ ),
+ ],
+ ),
+ ),
+ floatingActionButton: FloatingActionButton(
+ onPressed: _incrementCounter,
+ tooltip: 'Increment',
+ child: const Icon(Icons.cake),
+ ), // This trailing comma makes auto-formatting nicer for build methods.
+ );
+ }
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/pubspec.yaml b/packages/pigeon/e2e_tests/test_objc/pubspec.yaml
new file mode 100644
index 0000000..b66d931
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/pubspec.yaml
@@ -0,0 +1,71 @@
+name: test_objc
+description: A new Flutter project.
+
+# The following defines the version and build number for your application.
+# A version number is three numbers separated by dots, like 1.2.43
+# followed by an optional build number separated by a +.
+# Both the version and the builder number may be overridden in flutter
+# build by specifying --build-name and --build-number, respectively.
+# In Android, build-name is used as versionName while build-number used as versionCode.
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
+# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+version: 1.0.0+1
+
+environment:
+ sdk: ">=2.1.0 <3.0.0"
+
+dependencies:
+ # The following adds the Cupertino Icons font to your application.
+ # Use with the CupertinoIcons class for iOS style icons.
+ cupertino_icons: ^0.1.2
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ e2e: ^0.4.0
+ flutter_test:
+ sdk: flutter
+
+# 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/pigeon/e2e_tests/test_objc/test_driver/e2e_test.dart b/packages/pigeon/e2e_tests/test_objc/test_driver/e2e_test.dart
new file mode 100644
index 0000000..e97639a
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/test_driver/e2e_test.dart
@@ -0,0 +1,31 @@
+import 'package:e2e/e2e.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:test_objc/dartle.dart';
+
+void main() {
+ E2EWidgetsFlutterBinding.ensureInitialized();
+ testWidgets('simple call', (WidgetTester tester) async {
+ final SearchRequest request = SearchRequest()..query = 'Aaron';
+ final Api api = Api();
+ final SearchReply reply = await api.search(request);
+ expect(reply.result, equals('Hello Aaron!'));
+ });
+
+ testWidgets('simple nested', (WidgetTester tester) async {
+ final SearchRequest request = SearchRequest()..query = 'Aaron';
+ final Nested nested = Nested()..request = request;
+ final NestedApi api = NestedApi();
+ final SearchReply reply = await api.search(nested);
+ expect(reply.result, equals('Hello Aaron!'));
+ });
+
+ testWidgets('throws', (WidgetTester tester) async {
+ final SearchRequest request = SearchRequest()..query = 'error';
+ final Api api = Api();
+ SearchReply reply;
+ expect(() async {
+ reply = await api.search(request);
+ }, throwsException);
+ expect(reply, isNull);
+ });
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/test_driver/widget_test.dart b/packages/pigeon/e2e_tests/test_objc/test_driver/widget_test.dart
new file mode 100644
index 0000000..300082d
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/test_driver/widget_test.dart
@@ -0,0 +1,30 @@
+// 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:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:test_objc/main.dart';
+
+void main() {
+ testWidgets('Counter increments smoke test', (WidgetTester tester) async {
+ // Build our app and trigger a frame.
+ await tester.pumpWidget(const MyApp());
+
+ // Verify that our counter starts at 0.
+ expect(find.text('0'), findsOneWidget);
+ expect(find.text('1'), findsNothing);
+
+ // Tap the '+' icon and trigger a frame.
+ await tester.tap(find.byIcon(Icons.add));
+ await tester.pump();
+
+ // Verify that our counter has incremented.
+ expect(find.text('0'), findsNothing);
+ expect(find.text('1'), findsOneWidget);
+ });
+}
diff --git a/packages/pigeon/example/README.md b/packages/pigeon/example/README.md
new file mode 100644
index 0000000..a6d18e3
--- /dev/null
+++ b/packages/pigeon/example/README.md
@@ -0,0 +1,126 @@
+# Pigeon Examples
+
+## HostApi Example
+
+This example gives an overview of how to use Pigeon to call into the
+host-platform from Flutter.
+
+### message.dart
+
+This is the Pigeon file that describes the interface that will be used to call
+from Flutter to the host-platform.
+
+```dart
+import 'package:pigeon/pigeon.dart';
+
+class SearchRequest {
+ String? query;
+}
+
+class SearchReply {
+ String? result;
+}
+
+@HostApi()
+abstract class Api {
+ SearchReply search(SearchRequest request);
+}
+```
+
+### invocation
+
+This is the call to Pigeon that will injest `message.dart` and generate the code
+for iOS and Android.
+
+```sh
+flutter pub run pigeon \
+ --input pigeons/message.dart \
+ --dart_out lib/pigeon.dart \
+ --objc_header_out ios/Runner/pigeon.h \
+ --objc_source_out ios/Runner/pigeon.m \
+ --java_out ./android/app/src/main/java/dev/flutter/pigeon/Pigeon.java \
+ --java_package "dev.flutter.pigeon"
+```
+
+### AppDelegate.m
+
+This is the code that will use the generated Objective-C code to recieve calls
+from Flutter.
+
+```objc
+#import "AppDelegate.h"
+#import <Flutter/Flutter.h>
+#import "pigeon.h"
+
+@interface MyApi : NSObject <Api>
+@end
+
+@implementation MyApi
+-(SearchReply*)search:(SearchRequest*)request error:(FlutterError **)error {
+ SearchReply *reply = [[SearchReply alloc] init];
+ reply.result =
+ [NSString stringWithFormat:@"Hi %@!", request.query];
+ return reply;
+}
+@end
+
+- (BOOL)application:(UIApplication *)application
+didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions {
+ MyApi *api = [[MyApi alloc] init];
+ ApiSetup(getFlutterEngine().binaryMessenger, api);
+ return YES;
+}
+```
+
+### StartActivity.java
+
+This is the code that will use the generated Java code to receive calls from Flutter.
+
+```java
+import dev.flutter.pigeon.Pigeon;
+
+public class StartActivity extends Activity {
+ private class MyApi extends Pigeon.Api {
+ Pigeon.SearchReply search(Pigeon.SearchRequest request) {
+ Pigeon.SearchReply reply = new Pigeon.SearchReply();
+ reply.result = String.format("Hi %s!", request.query);
+ return reply;
+ }
+ }
+
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Pigeon.SetupApi(getBinaryMessenger(), new MyApi());
+ }
+}
+```
+
+### test.dart
+
+This is the Dart code that will call into the host-platform using the generated
+Dart code.
+
+```dart
+import 'pigeon.dart';
+
+void main() {
+ testWidgets("test pigeon", (WidgetTester tester) async {
+ SearchRequest request = SearchRequest()..query = "Aaron";
+ Api api = Api();
+ SearchReply reply = await api.search(request);
+ expect(reply.result, equals("Hi Aaron!"));
+ });
+}
+
+```
+
+## Swift / Kotlin Plugin Example
+
+A downloadable example of using Pigeon to create a Flutter Plugin with Swift and
+Kotlin can be found at
+[gaaclarke/flutter_plugin_example](https://github.com/gaaclarke/pigeon_plugin_example).
+
+## Swift / Kotlin Add-to-app Example
+
+A full example of using Pigeon for add-to-app with Swift on iOS can be found at
+[samples/add_to_app/books](https://github.com/flutter/samples/tree/master/add_to_app/books).
diff --git a/packages/pigeon/lib/ast.dart b/packages/pigeon/lib/ast.dart
new file mode 100644
index 0000000..95fb026
--- /dev/null
+++ b/packages/pigeon/lib/ast.dart
@@ -0,0 +1,121 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Enum that represents where an [Api] is located, on the host or Flutter.
+enum ApiLocation {
+ /// The API is for calling functions defined on the host.
+ host,
+
+ /// The API is for calling functions defined in Flutter.
+ flutter,
+}
+
+/// Superclass for all AST nodes.
+class Node {}
+
+/// Represents a method on an [Api].
+class Method extends Node {
+ /// Parametric constructor for [Method].
+ Method({
+ required this.name,
+ required this.returnType,
+ required this.argType,
+ this.isAsynchronous = false,
+ });
+
+ /// The name of the method.
+ String name;
+
+ /// The data-type of the return value.
+ String returnType;
+
+ /// The data-type of the argument.
+ String argType;
+
+ /// Whether the receiver of this method is expected to return synchronously or not.
+ bool isAsynchronous;
+}
+
+/// Represents a collection of [Method]s that are hosted on a given [location].
+class Api extends Node {
+ /// Parametric constructor for [Api].
+ Api({
+ required this.name,
+ required this.location,
+ required this.methods,
+ this.dartHostTestHandler,
+ });
+
+ /// The name of the API.
+ String name;
+
+ /// Where the API's implementation is located, host or Flutter.
+ ApiLocation location;
+
+ /// List of methods inside the API.
+ List<Method> methods;
+
+ /// The name of the Dart test interface to generate to help with testing.
+ String? dartHostTestHandler;
+}
+
+/// Represents a field on a [Class].
+class Field extends Node {
+ /// Parametric constructor for [Field].
+ Field({
+ required this.name,
+ required this.dataType,
+ });
+
+ /// The name of the field.
+ String name;
+
+ /// The data-type of the field (ex 'String' or 'int').
+ String dataType;
+
+ @override
+ String toString() {
+ return '(Field name:$name)';
+ }
+}
+
+/// Represents a class with [Field]s.
+class Class extends Node {
+ /// Parametric constructor for [Class].
+ Class({
+ required this.name,
+ required this.fields,
+ });
+
+ /// The name of the class.
+ String name;
+
+ /// All the fields contained in the class.
+ List<Field> fields;
+
+ @override
+ String toString() {
+ return '(Class name:$name fields:$fields)';
+ }
+}
+
+/// Top-level node for the AST.
+class Root extends Node {
+ /// Parametric constructor for [Root].
+ Root({
+ required this.classes,
+ required this.apis,
+ });
+
+ /// All the classes contained in the AST.
+ List<Class> classes;
+
+ /// All the API's contained in the AST.
+ List<Api> apis;
+
+ @override
+ String toString() {
+ return '(Root classes:$classes apis:$apis)';
+ }
+}
diff --git a/packages/pigeon/lib/dart_generator.dart b/packages/pigeon/lib/dart_generator.dart
new file mode 100644
index 0000000..56fdc22
--- /dev/null
+++ b/packages/pigeon/lib/dart_generator.dart
@@ -0,0 +1,332 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'ast.dart';
+import 'generator_tools.dart';
+
+/// Options that control how Dart code will be generated.
+class DartOptions {
+ /// Constructor for DartOptions.
+ DartOptions({this.isNullSafe = true});
+
+ /// Determines if the generated code has null safety annotations (Dart >=2.12 required).
+ bool isNullSafe;
+}
+
+String _escapeForDartSingleQuotedString(String raw) {
+ return raw
+ .replaceAll(r'\', r'\\')
+ .replaceAll(r'$', r'\$')
+ .replaceAll(r"'", r"\'");
+}
+
+void _writeHostApi(DartOptions opt, Indent indent, Api api) {
+ assert(api.location == ApiLocation.host);
+ final String nullTag = opt.isNullSafe ? '?' : '';
+ final String unwrapOperator = opt.isNullSafe ? '!' : '';
+ bool first = true;
+ indent.write('class ${api.name} ');
+ indent.scoped('{', '}', () {
+ indent.format('''
+/// Constructor for [${api.name}]. The [binaryMessenger] named argument is
+/// available for dependency injection. If it is left null, the default
+/// BinaryMessenger will be used which routes to the host platform.
+${api.name}({BinaryMessenger$nullTag binaryMessenger}) : _binaryMessenger = binaryMessenger;
+
+final BinaryMessenger$nullTag _binaryMessenger;
+''');
+
+ for (final Method func in api.methods) {
+ if (!first) {
+ indent.writeln('');
+ } else {
+ first = false;
+ }
+ String argSignature = '';
+ String sendArgument = 'null';
+ String? encodedDeclaration;
+ if (func.argType != 'void') {
+ argSignature = '${func.argType} arg';
+ sendArgument = 'encoded';
+ encodedDeclaration = 'final Object encoded = arg.encode();';
+ }
+ indent.write(
+ 'Future<${func.returnType}> ${func.name}($argSignature) async ',
+ );
+ indent.scoped('{', '}', () {
+ if (encodedDeclaration != null) {
+ indent.writeln(encodedDeclaration);
+ }
+ final String channelName = makeChannelName(api, func);
+ indent.writeln(
+ 'final BasicMessageChannel<Object$nullTag> channel = BasicMessageChannel<Object$nullTag>(');
+ indent.nest(2, () {
+ indent.writeln(
+ '\'$channelName\', const StandardMessageCodec(), binaryMessenger: _binaryMessenger);',
+ );
+ });
+ final String returnStatement = func.returnType == 'void'
+ ? '// noop'
+ : 'return ${func.returnType}.decode(replyMap[\'${Keys.result}\']$unwrapOperator);';
+ indent.format('''
+final Map<Object$nullTag, Object$nullTag>$nullTag replyMap =\n\t\tawait channel.send($sendArgument) as Map<Object$nullTag, Object$nullTag>$nullTag;
+if (replyMap == null) {
+\tthrow PlatformException(
+\t\tcode: 'channel-error',
+\t\tmessage: 'Unable to establish connection on channel.',
+\t\tdetails: null,
+\t);
+} else if (replyMap['error'] != null) {
+\tfinal Map<Object$nullTag, Object$nullTag> error = (replyMap['${Keys.error}'] as Map<Object$nullTag, Object$nullTag>$nullTag)$unwrapOperator;
+\tthrow PlatformException(
+\t\tcode: (error['${Keys.errorCode}'] as String$nullTag)$unwrapOperator,
+\t\tmessage: error['${Keys.errorMessage}'] as String$nullTag,
+\t\tdetails: error['${Keys.errorDetails}'],
+\t);
+} else {
+\t$returnStatement
+}''');
+ });
+ }
+ });
+}
+
+void _writeFlutterApi(
+ DartOptions opt,
+ Indent indent,
+ Api api, {
+ String Function(Method)? channelNameFunc,
+ bool isMockHandler = false,
+}) {
+ assert(api.location == ApiLocation.flutter);
+ final String nullTag = opt.isNullSafe ? '?' : '';
+ final String unwrapOperator = opt.isNullSafe ? '!' : '';
+ indent.write('abstract class ${api.name} ');
+ indent.scoped('{', '}', () {
+ for (final Method func in api.methods) {
+ final bool isAsync = func.isAsynchronous;
+ final String returnType =
+ isAsync ? 'Future<${func.returnType}>' : func.returnType;
+ final String argSignature =
+ func.argType == 'void' ? '' : '${func.argType} arg';
+ indent.writeln('$returnType ${func.name}($argSignature);');
+ }
+ indent.write('static void setup(${api.name}$nullTag api) ');
+ indent.scoped('{', '}', () {
+ for (final Method func in api.methods) {
+ indent.write('');
+ indent.scoped('{', '}', () {
+ indent.writeln(
+ 'const BasicMessageChannel<Object$nullTag> channel = BasicMessageChannel<Object$nullTag>(',
+ );
+ final String channelName = channelNameFunc == null
+ ? makeChannelName(api, func)
+ : channelNameFunc(func);
+ indent.nest(2, () {
+ indent.writeln(
+ '\'$channelName\', StandardMessageCodec());',
+ );
+ });
+ final String messageHandlerSetter =
+ isMockHandler ? 'setMockMessageHandler' : 'setMessageHandler';
+ indent.write('if (api == null) ');
+ indent.scoped('{', '}', () {
+ indent.writeln('channel.$messageHandlerSetter(null);');
+ }, addTrailingNewline: false);
+ indent.add(' else ');
+ indent.scoped('{', '}', () {
+ indent.write(
+ 'channel.$messageHandlerSetter((Object$nullTag message) async ',
+ );
+ indent.scoped('{', '});', () {
+ final String argType = func.argType;
+ final String returnType = func.returnType;
+ final bool isAsync = func.isAsynchronous;
+ final String emptyReturnStatement = isMockHandler
+ ? 'return <Object$nullTag, Object$nullTag>{};'
+ : func.returnType == 'void'
+ ? 'return;'
+ : 'return null;';
+ String call;
+ if (argType == 'void') {
+ indent.writeln('// ignore message');
+ call = 'api.${func.name}()';
+ } else {
+ indent.writeln(
+ 'assert(message != null, \'Argument for $channelName was null. Expected $argType.\');',
+ );
+ indent.writeln(
+ 'final $argType input = $argType.decode(message$unwrapOperator);',
+ );
+ call = 'api.${func.name}(input)';
+ }
+ if (returnType == 'void') {
+ if (isAsync) {
+ indent.writeln('await $call;');
+ } else {
+ indent.writeln('$call;');
+ }
+ indent.writeln(emptyReturnStatement);
+ } else {
+ if (isAsync) {
+ indent.writeln('final $returnType output = await $call;');
+ } else {
+ indent.writeln('final $returnType output = $call;');
+ }
+ const String returnExpression = 'output.encode()';
+ final String returnStatement = isMockHandler
+ ? 'return <Object$nullTag, Object$nullTag>{\'${Keys.result}\': $returnExpression};'
+ : 'return $returnExpression;';
+ indent.writeln(returnStatement);
+ }
+ });
+ });
+ });
+ }
+ });
+ });
+}
+
+String _addGenericTypes(String dataType, String nullTag) {
+ switch (dataType) {
+ case 'List':
+ return 'List<Object$nullTag>$nullTag';
+ case 'Map':
+ return 'Map<Object$nullTag, Object$nullTag>$nullTag';
+ default:
+ return '$dataType$nullTag';
+ }
+}
+
+/// Generates Dart source code for the given AST represented by [root],
+/// outputting the code to [sink].
+void generateDart(DartOptions opt, Root root, StringSink sink) {
+ final String nullTag = opt.isNullSafe ? '?' : '';
+ final String unwrapOperator = opt.isNullSafe ? '!' : '';
+ final List<String> customClassNames =
+ root.classes.map((Class x) => x.name).toList();
+ final Indent indent = Indent(sink);
+ indent.writeln('// $generatedCodeWarning');
+ indent.writeln('// $seeAlsoWarning');
+ indent.writeln(
+ '// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types',
+ );
+ indent.writeln('// @dart = ${opt.isNullSafe ? '2.12' : '2.8'}');
+ indent.writeln('import \'dart:async\';');
+ indent.writeln(
+ 'import \'dart:typed_data\' show Uint8List, Int32List, Int64List, Float64List;',
+ );
+ indent.writeln('');
+ indent.writeln('import \'package:flutter/services.dart\';');
+ for (final Class klass in root.classes) {
+ indent.writeln('');
+ sink.write('class ${klass.name} ');
+ indent.scoped('{', '}', () {
+ for (final Field field in klass.fields) {
+ final String datatype = _addGenericTypes(field.dataType, nullTag);
+ indent.writeln('$datatype ${field.name};');
+ }
+ if (klass.fields.isNotEmpty) {
+ indent.writeln('');
+ }
+ indent.write('Object encode() ');
+ indent.scoped('{', '}', () {
+ indent.writeln(
+ 'final Map<Object$nullTag, Object$nullTag> pigeonMap = <Object$nullTag, Object$nullTag>{};',
+ );
+ for (final Field field in klass.fields) {
+ indent.write('pigeonMap[\'${field.name}\'] = ');
+ if (customClassNames.contains(field.dataType)) {
+ indent.addln(
+ '${field.name} == null ? null : ${field.name}$unwrapOperator.encode();',
+ );
+ } else {
+ indent.addln('${field.name};');
+ }
+ }
+ indent.writeln('return pigeonMap;');
+ });
+ indent.writeln('');
+ indent.write(
+ 'static ${klass.name} decode(Object message) ',
+ );
+ indent.scoped('{', '}', () {
+ indent.writeln(
+ 'final Map<Object$nullTag, Object$nullTag> pigeonMap = message as Map<Object$nullTag, Object$nullTag>;',
+ );
+ indent.writeln('return ${klass.name}()');
+ indent.nest(1, () {
+ for (int index = 0; index < klass.fields.length; index += 1) {
+ final Field field = klass.fields[index];
+ indent.write('..${field.name} = ');
+ if (customClassNames.contains(field.dataType)) {
+ indent.format('''
+pigeonMap['${field.name}'] != null
+\t\t? ${field.dataType}.decode(pigeonMap['${field.name}']$unwrapOperator)
+\t\t: null''', leadingSpace: false, trailingNewline: false);
+ } else {
+ indent.add(
+ 'pigeonMap[\'${field.name}\'] as ${_addGenericTypes(field.dataType, nullTag)}',
+ );
+ }
+ indent.addln(index == klass.fields.length - 1 ? ';' : '');
+ }
+ });
+ });
+ });
+ }
+ for (final Api api in root.apis) {
+ indent.writeln('');
+ if (api.location == ApiLocation.host) {
+ _writeHostApi(opt, indent, api);
+ } else if (api.location == ApiLocation.flutter) {
+ _writeFlutterApi(opt, indent, api);
+ }
+ }
+}
+
+/// Generates Dart source code for test support libraries based on the
+/// given AST represented by [root], outputting the code to [sink].
+void generateTestDart(
+ DartOptions opt,
+ Root root,
+ StringSink sink,
+ String mainDartFile,
+) {
+ final Indent indent = Indent(sink);
+ indent.writeln('// $generatedCodeWarning');
+ indent.writeln('// $seeAlsoWarning');
+ indent.writeln(
+ '// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import',
+ );
+ indent.writeln('// @dart = ${opt.isNullSafe ? '2.12' : '2.8'}');
+ indent.writeln('import \'dart:async\';');
+ indent.writeln(
+ 'import \'dart:typed_data\' show Uint8List, Int32List, Int64List, Float64List;',
+ );
+ indent.writeln('import \'package:flutter/services.dart\';');
+ indent.writeln('import \'package:flutter_test/flutter_test.dart\';');
+ indent.writeln('');
+ indent.writeln(
+ 'import \'${_escapeForDartSingleQuotedString(mainDartFile)}\';',
+ );
+ for (final Api api in root.apis) {
+ if (api.location == ApiLocation.host && api.dartHostTestHandler != null) {
+ final Api mockApi = Api(
+ name: api.dartHostTestHandler!,
+ methods: api.methods,
+ location: ApiLocation.flutter,
+ dartHostTestHandler: api.dartHostTestHandler,
+ );
+ indent.writeln('');
+ _writeFlutterApi(
+ opt,
+ indent,
+ mockApi,
+ channelNameFunc: (Method func) => makeChannelName(api, func),
+ isMockHandler: true,
+ );
+ }
+ }
+}
diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart
new file mode 100644
index 0000000..44500fc
--- /dev/null
+++ b/packages/pigeon/lib/generator_tools.dart
@@ -0,0 +1,208 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io';
+import 'dart:mirrors';
+import 'ast.dart';
+
+/// The current version of pigeon. This must match the version in pubspec.yaml.
+const String pigeonVersion = '0.2.1';
+
+/// Read all the content from [stdin] to a String.
+String readStdin() {
+ final List<int> bytes = <int>[];
+ int byte = stdin.readByteSync();
+ while (byte >= 0) {
+ bytes.add(byte);
+ byte = stdin.readByteSync();
+ }
+ return utf8.decode(bytes);
+}
+
+/// A helper class for managing indentation, wrapping a [StringSink].
+class Indent {
+ /// Constructor which takes a [StringSink] [Ident] will wrap.
+ Indent(this._sink);
+
+ int _count = 0;
+ final StringSink _sink;
+
+ /// String used for newlines (ex "\n").
+ final String newline = '\n';
+
+ /// String used to represent a tab.
+ final String tab = ' ';
+
+ /// Increase the indentation level.
+ void inc([int level = 1]) {
+ _count += level;
+ }
+
+ /// Decrement the indentation level.
+ void dec([int level = 1]) {
+ _count -= level;
+ }
+
+ /// Returns the String representing the current indentation.
+ String str() {
+ String result = '';
+ for (int i = 0; i < _count; i++) {
+ result += tab;
+ }
+ return result;
+ }
+
+ /// Replaces the newlines and tabs of input and adds it to the stream.
+ void format(String input,
+ {bool leadingSpace = true, bool trailingNewline = true}) {
+ final List<String> lines = input.split('\n');
+ for (int i = 0; i < lines.length; ++i) {
+ final String line = lines[i];
+ if (i == 0 && !leadingSpace) {
+ addln(line.replaceAll('\t', tab));
+ } else if (i == lines.length - 1 && !trailingNewline) {
+ write(line.replaceAll('\t', tab));
+ } else {
+ writeln(line.replaceAll('\t', tab));
+ }
+ }
+ }
+
+ /// Scoped increase of the ident level. For the execution of [func] the
+ /// indentation will be incremented.
+ void scoped(
+ String? begin,
+ String? end,
+ Function func, {
+ bool addTrailingNewline = true,
+ }) {
+ if (begin != null) {
+ _sink.write(begin + newline);
+ }
+ nest(1, func);
+ if (end != null) {
+ _sink.write(str() + end);
+ if (addTrailingNewline) {
+ _sink.write(newline);
+ }
+ }
+ }
+
+ /// Like `scoped` but writes the current indentation level.
+ void writeScoped(
+ String? begin,
+ String end,
+ Function func, {
+ bool addTrailingNewline = true,
+ }) {
+ scoped(str() + (begin ?? ''), end, func,
+ addTrailingNewline: addTrailingNewline);
+ }
+
+ /// Scoped increase of the ident level. For the execution of [func] the
+ /// indentation will be incremented by the given amount.
+ void nest(int count, Function func) {
+ inc(count);
+ func();
+ dec(count);
+ }
+
+ /// Add [text] with indentation and a newline.
+ void writeln(String text) {
+ if (text.isEmpty) {
+ _sink.write(newline);
+ } else {
+ _sink.write(str() + text + newline);
+ }
+ }
+
+ /// Add [text] with indentation.
+ void write(String text) {
+ _sink.write(str() + text);
+ }
+
+ /// Add [text] with a newline.
+ void addln(String text) {
+ _sink.write(text + newline);
+ }
+
+ /// Just adds [text].
+ void add(String text) {
+ _sink.write(text);
+ }
+}
+
+/// Create the generated channel name for a [func] on a [api].
+String makeChannelName(Api api, Method func) {
+ return 'dev.flutter.pigeon.${api.name}.${func.name}';
+}
+
+/// Represents the mapping of a Dart datatype to a Host datatype.
+class HostDatatype {
+ /// Parametric constructor for HostDatatype.
+ HostDatatype({
+ required this.datatype,
+ required this.isBuiltin,
+ });
+
+ /// The [String] that can be printed into host code to represent the type.
+ final String datatype;
+
+ /// `true` if the host datatype is something builtin.
+ final bool isBuiltin;
+}
+
+/// Calculates the [HostDatatype] for the provided [Field]. It will check the
+/// field against the `classes` to check if it is a builtin type.
+/// `builtinResolver` will return the host datatype for the Dart datatype for
+/// builtin types. `customResolver` can modify the datatype of custom types.
+HostDatatype getHostDatatype(
+ Field field, List<Class> classes, String? Function(String) builtinResolver,
+ {String Function(String)? customResolver}) {
+ final String? datatype = builtinResolver(field.dataType);
+ if (datatype == null) {
+ if (classes.map((Class x) => x.name).contains(field.dataType)) {
+ final String customName = customResolver != null
+ ? customResolver(field.dataType)
+ : field.dataType;
+ return HostDatatype(datatype: customName, isBuiltin: false);
+ } else {
+ throw Exception(
+ 'unrecognized datatype for field:"${field.name}" of type:"${field.dataType}"');
+ }
+ } else {
+ return HostDatatype(datatype: datatype, isBuiltin: true);
+ }
+}
+
+/// Warning printed at the top of all generated code.
+const String generatedCodeWarning =
+ 'Autogenerated from Pigeon (v$pigeonVersion), do not edit directly.';
+
+/// String to be printed after `generatedCodeWarning`.
+const String seeAlsoWarning = 'See also: https://pub.dev/packages/pigeon';
+
+/// Collection of keys used in dictionaries across generators.
+class Keys {
+ /// The key in the result hash for the 'result' value.
+ static const String result = 'result';
+
+ /// The key in the result hash for the 'error' value.
+ static const String error = 'error';
+
+ /// The key in an error hash for the 'code' value.
+ static const String errorCode = 'code';
+
+ /// The key in an error hash for the 'message' value.
+ static const String errorMessage = 'message';
+
+ /// The key in an error hash for the 'details' value.
+ static const String errorDetails = 'details';
+}
+
+/// Returns true if `type` represents 'void'.
+bool isVoid(TypeMirror type) {
+ return MirrorSystem.getName(type.simpleName) == 'void';
+}
diff --git a/packages/pigeon/lib/java_generator.dart b/packages/pigeon/lib/java_generator.dart
new file mode 100644
index 0000000..5f8efca
--- /dev/null
+++ b/packages/pigeon/lib/java_generator.dart
@@ -0,0 +1,320 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'ast.dart';
+import 'generator_tools.dart';
+
+const Map<String, String> _javaTypeForDartTypeMap = <String, String>{
+ 'bool': 'Boolean',
+ 'int': 'Long',
+ 'String': 'String',
+ 'double': 'Double',
+ 'Uint8List': 'byte[]',
+ 'Int32List': 'int[]',
+ 'Int64List': 'long[]',
+ 'Float64List': 'double[]',
+ 'List': 'List<Object>',
+ 'Map': 'Map<Object, Object>',
+};
+
+/// Options that control how Java code will be generated.
+class JavaOptions {
+ /// Creates a [JavaOptions] object
+ JavaOptions({
+ this.className,
+ this.package,
+ });
+
+ /// The name of the class that will house all the generated classes.
+ String? className;
+
+ /// The package where the generated class will live.
+ String? package;
+}
+
+void _writeHostApi(Indent indent, Api api) {
+ assert(api.location == ApiLocation.host);
+
+ indent.writeln(
+ '/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/');
+ indent.write('public interface ${api.name} ');
+ indent.scoped('{', '}', () {
+ for (final Method method in api.methods) {
+ final String returnType =
+ method.isAsynchronous ? 'void' : method.returnType;
+ final List<String> argSignature = <String>[];
+ if (method.argType != 'void') {
+ argSignature.add('${method.argType} arg');
+ }
+ if (method.isAsynchronous) {
+ final String returnType =
+ method.returnType == 'void' ? 'Void' : method.returnType;
+ argSignature.add('Result<$returnType> result');
+ }
+ indent.writeln('$returnType ${method.name}(${argSignature.join(', ')});');
+ }
+ indent.addln('');
+ indent.writeln(
+ '/** Sets up an instance of `${api.name}` to handle messages through the `binaryMessenger`. */');
+ indent.write(
+ 'static void setup(BinaryMessenger binaryMessenger, ${api.name} api) ');
+ indent.scoped('{', '}', () {
+ for (final Method method in api.methods) {
+ final String channelName = makeChannelName(api, method);
+ indent.write('');
+ indent.scoped('{', '}', () {
+ indent.writeln('BasicMessageChannel<Object> channel =');
+ indent.inc();
+ indent.inc();
+ indent.writeln(
+ 'new BasicMessageChannel<>(binaryMessenger, "$channelName", new StandardMessageCodec());');
+ indent.dec();
+ indent.dec();
+ indent.write('if (api != null) ');
+ indent.scoped('{', '} else {', () {
+ indent.write('channel.setMessageHandler((message, reply) -> ');
+ indent.scoped('{', '});', () {
+ final String argType = method.argType;
+ final String returnType = method.returnType;
+ indent.writeln('Map<String, Object> wrapped = new HashMap<>();');
+ indent.write('try ');
+ indent.scoped('{', '}', () {
+ final List<String> methodArgument = <String>[];
+ if (argType != 'void') {
+ indent.writeln('@SuppressWarnings("ConstantConditions")');
+ indent.writeln(
+ '$argType input = $argType.fromMap((Map<String, Object>)message);');
+ methodArgument.add('input');
+ }
+ if (method.isAsynchronous) {
+ final String resultValue =
+ method.returnType == 'void' ? 'null' : 'result.toMap()';
+ methodArgument.add(
+ 'result -> { '
+ 'wrapped.put("${Keys.result}", $resultValue); '
+ 'reply.reply(wrapped); '
+ '}',
+ );
+ }
+ final String call =
+ 'api.${method.name}(${methodArgument.join(', ')})';
+ if (method.isAsynchronous) {
+ indent.writeln('$call;');
+ } else if (method.returnType == 'void') {
+ indent.writeln('$call;');
+ indent.writeln('wrapped.put("${Keys.result}", null);');
+ } else {
+ indent.writeln('$returnType output = $call;');
+ indent.writeln(
+ 'wrapped.put("${Keys.result}", output.toMap());');
+ }
+ });
+ indent.write('catch (Error | RuntimeException exception) ');
+ indent.scoped('{', '}', () {
+ indent.writeln(
+ 'wrapped.put("${Keys.error}", wrapError(exception));');
+ if (method.isAsynchronous) {
+ indent.writeln('reply.reply(wrapped);');
+ }
+ });
+ if (!method.isAsynchronous) {
+ indent.writeln('reply.reply(wrapped);');
+ }
+ });
+ });
+ indent.scoped(null, '}', () {
+ indent.writeln('channel.setMessageHandler(null);');
+ });
+ });
+ }
+ });
+ });
+}
+
+void _writeFlutterApi(Indent indent, Api api) {
+ assert(api.location == ApiLocation.flutter);
+ indent.writeln(
+ '/** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/');
+ indent.write('public static class ${api.name} ');
+ indent.scoped('{', '}', () {
+ indent.writeln('private final BinaryMessenger binaryMessenger;');
+ indent.write('public ${api.name}(BinaryMessenger argBinaryMessenger)');
+ indent.scoped('{', '}', () {
+ indent.writeln('this.binaryMessenger = argBinaryMessenger;');
+ });
+ indent.write('public interface Reply<T> ');
+ indent.scoped('{', '}', () {
+ indent.writeln('void reply(T reply);');
+ });
+ for (final Method func in api.methods) {
+ final String channelName = makeChannelName(api, func);
+ final String returnType =
+ func.returnType == 'void' ? 'Void' : func.returnType;
+ String sendArgument;
+ if (func.argType == 'void') {
+ indent.write('public void ${func.name}(Reply<$returnType> callback) ');
+ sendArgument = 'null';
+ } else {
+ indent.write(
+ 'public void ${func.name}(${func.argType} argInput, Reply<$returnType> callback) ');
+ sendArgument = 'inputMap';
+ }
+ indent.scoped('{', '}', () {
+ indent.writeln('BasicMessageChannel<Object> channel =');
+ indent.inc();
+ indent.inc();
+ indent.writeln(
+ 'new BasicMessageChannel<>(binaryMessenger, "$channelName", new StandardMessageCodec());');
+ indent.dec();
+ indent.dec();
+ if (func.argType != 'void') {
+ indent.writeln('Map<String, Object> inputMap = argInput.toMap();');
+ }
+ indent.write('channel.send($sendArgument, channelReply -> ');
+ indent.scoped('{', '});', () {
+ if (func.returnType == 'void') {
+ indent.writeln('callback.reply(null);');
+ } else {
+ indent.writeln('Map outputMap = (Map)channelReply;');
+ indent.writeln('@SuppressWarnings("ConstantConditions")');
+ indent.writeln(
+ '${func.returnType} output = ${func.returnType}.fromMap(outputMap);');
+ indent.writeln('callback.reply(output);');
+ }
+ });
+ });
+ }
+ });
+}
+
+String _makeGetter(Field field) {
+ final String uppercased =
+ field.name.substring(0, 1).toUpperCase() + field.name.substring(1);
+ return 'get$uppercased';
+}
+
+String _makeSetter(Field field) {
+ final String uppercased =
+ field.name.substring(0, 1).toUpperCase() + field.name.substring(1);
+ return 'set$uppercased';
+}
+
+String? _javaTypeForDartType(String datatype) {
+ return _javaTypeForDartTypeMap[datatype];
+}
+
+String _castObject(Field field, List<Class> classes, String varName) {
+ final HostDatatype hostDatatype =
+ getHostDatatype(field, classes, _javaTypeForDartType);
+ if (field.dataType == 'int') {
+ return '($varName == null) ? null : (($varName instanceof Integer) ? (Integer)$varName : (${hostDatatype.datatype})$varName)';
+ } else if (!hostDatatype.isBuiltin &&
+ classes.map((Class x) => x.name).contains(field.dataType)) {
+ return '${hostDatatype.datatype}.fromMap((Map)$varName)';
+ } else {
+ return '(${hostDatatype.datatype})$varName';
+ }
+}
+
+/// Generates the ".java" file for the AST represented by [root] to [sink] with the
+/// provided [options].
+void generateJava(JavaOptions options, Root root, StringSink sink) {
+ final Set<String> rootClassNameSet =
+ root.classes.map((Class x) => x.name).toSet();
+ final Indent indent = Indent(sink);
+ indent.writeln('// $generatedCodeWarning');
+ indent.writeln('// $seeAlsoWarning');
+ indent.addln('');
+ if (options.package != null) {
+ indent.writeln('package ${options.package};');
+ }
+ indent.addln('');
+ indent.writeln('import io.flutter.plugin.common.BasicMessageChannel;');
+ indent.writeln('import io.flutter.plugin.common.BinaryMessenger;');
+ indent.writeln('import io.flutter.plugin.common.StandardMessageCodec;');
+ indent.writeln('import java.util.List;');
+ indent.writeln('import java.util.Map;');
+ indent.writeln('import java.util.HashMap;');
+
+ indent.addln('');
+ indent.writeln('/** Generated class from Pigeon. */');
+ indent.writeln(
+ '@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})');
+ indent.write('public class ${options.className!} ');
+ indent.scoped('{', '}', () {
+ for (final Class klass in root.classes) {
+ indent.addln('');
+ indent.writeln(
+ '/** Generated class from Pigeon that represents data sent in messages. */');
+ indent.write('public static class ${klass.name} ');
+ indent.scoped('{', '}', () {
+ for (final Field field in klass.fields) {
+ final HostDatatype hostDatatype =
+ getHostDatatype(field, root.classes, _javaTypeForDartType);
+ indent.writeln('private ${hostDatatype.datatype} ${field.name};');
+ indent.writeln(
+ 'public ${hostDatatype.datatype} ${_makeGetter(field)}() { return ${field.name}; }');
+ indent.writeln(
+ 'public void ${_makeSetter(field)}(${hostDatatype.datatype} setterArg) { this.${field.name} = setterArg; }');
+ indent.addln('');
+ }
+ indent.write('Map<String, Object> toMap() ');
+ indent.scoped('{', '}', () {
+ indent.writeln('Map<String, Object> toMapResult = new HashMap<>();');
+ for (final Field field in klass.fields) {
+ final HostDatatype hostDatatype =
+ getHostDatatype(field, root.classes, _javaTypeForDartType);
+ String toWriteValue = '';
+ if (!hostDatatype.isBuiltin &&
+ rootClassNameSet.contains(field.dataType)) {
+ toWriteValue = '${field.name}.toMap()';
+ } else {
+ toWriteValue = field.name;
+ }
+ indent.writeln('toMapResult.put("${field.name}", $toWriteValue);');
+ }
+ indent.writeln('return toMapResult;');
+ });
+ indent.write('static ${klass.name} fromMap(Map<String, Object> map) ');
+ indent.scoped('{', '}', () {
+ indent.writeln('${klass.name} fromMapResult = new ${klass.name}();');
+ for (final Field field in klass.fields) {
+ indent.writeln('Object ${field.name} = map.get("${field.name}");');
+ indent.writeln(
+ 'fromMapResult.${field.name} = ${_castObject(field, root.classes, field.name)};');
+ }
+ indent.writeln('return fromMapResult;');
+ });
+ });
+ }
+
+ if (root.apis.any((Api api) =>
+ api.location == ApiLocation.host &&
+ api.methods.any((Method it) => it.isAsynchronous))) {
+ indent.addln('');
+ indent.write('public interface Result<T> ');
+ indent.scoped('{', '}', () {
+ indent.writeln('void success(T result);');
+ });
+ }
+
+ for (final Api api in root.apis) {
+ indent.addln('');
+ if (api.location == ApiLocation.host) {
+ _writeHostApi(indent, api);
+ } else if (api.location == ApiLocation.flutter) {
+ _writeFlutterApi(indent, api);
+ }
+ }
+
+ indent.format('''
+private static Map<String, Object> wrapError(Throwable exception) {
+\tMap<String, Object> errorMap = new HashMap<>();
+\terrorMap.put("${Keys.errorMessage}", exception.toString());
+\terrorMap.put("${Keys.errorCode}", exception.getClass().getSimpleName());
+\terrorMap.put("${Keys.errorDetails}", null);
+\treturn errorMap;
+}''');
+ });
+}
diff --git a/packages/pigeon/lib/objc_generator.dart b/packages/pigeon/lib/objc_generator.dart
new file mode 100644
index 0000000..caec03d
--- /dev/null
+++ b/packages/pigeon/lib/objc_generator.dart
@@ -0,0 +1,449 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'ast.dart';
+import 'generator_tools.dart';
+
+/// Options that control how Objective-C code will be generated.
+class ObjcOptions {
+ /// Parametric constructor for ObjcOptions.
+ ObjcOptions({
+ this.header,
+ this.prefix,
+ });
+
+ /// The path to the header that will get placed in the source filed (example:
+ /// "foo.h").
+ String? header;
+
+ /// Prefix that will be appended before all generated classes and protocols.
+ String? prefix;
+}
+
+String _className(String? prefix, String className) {
+ if (prefix != null) {
+ return '$prefix$className';
+ } else {
+ return className;
+ }
+}
+
+String _callbackForType(String dartType, String objcType) {
+ return dartType == 'void'
+ ? 'void(^)(NSError* _Nullable)'
+ : 'void(^)($objcType*, NSError* _Nullable)';
+}
+
+const Map<String, String> _objcTypeForDartTypeMap = <String, String>{
+ 'bool': 'NSNumber *',
+ 'int': 'NSNumber *',
+ 'String': 'NSString *',
+ 'double': 'NSNumber *',
+ 'Uint8List': 'FlutterStandardTypedData *',
+ 'Int32List': 'FlutterStandardTypedData *',
+ 'Int64List': 'FlutterStandardTypedData *',
+ 'Float64List': 'FlutterStandardTypedData *',
+ 'List': 'NSArray *',
+ 'Map': 'NSDictionary *',
+};
+
+const Map<String, String> _propertyTypeForDartTypeMap = <String, String>{
+ 'String': 'copy',
+ 'bool': 'strong',
+ 'int': 'strong',
+ 'double': 'strong',
+ 'Uint8List': 'strong',
+ 'Int32List': 'strong',
+ 'Int64List': 'strong',
+ 'Float64List': 'strong',
+ 'List': 'strong',
+ 'Map': 'strong',
+};
+
+String? _objcTypeForDartType(String type) {
+ return _objcTypeForDartTypeMap[type];
+}
+
+String _propertyTypeForDartType(String type) {
+ final String? result = _propertyTypeForDartTypeMap[type];
+ if (result == null) {
+ return 'assign';
+ } else {
+ return result;
+ }
+}
+
+void _writeClassDeclarations(
+ Indent indent, List<Class> classes, String? prefix) {
+ for (final Class klass in classes) {
+ indent.writeln('@interface ${_className(prefix, klass.name)} : NSObject');
+ for (final Field field in klass.fields) {
+ final HostDatatype hostDatatype = getHostDatatype(
+ field, classes, _objcTypeForDartType,
+ customResolver: (String x) => '${_className(prefix, x)} *');
+ final String propertyType = hostDatatype.isBuiltin
+ ? _propertyTypeForDartType(field.dataType)
+ : 'strong';
+ final String nullability =
+ hostDatatype.datatype.contains('*') ? ', nullable' : '';
+ indent.writeln(
+ '@property(nonatomic, $propertyType$nullability) ${hostDatatype.datatype} ${field.name};');
+ }
+ indent.writeln('@end');
+ indent.writeln('');
+ }
+}
+
+void _writeHostApiDeclaration(Indent indent, Api api, ObjcOptions options) {
+ final String apiName = _className(options.prefix, api.name);
+ indent.writeln('@protocol $apiName');
+ for (final Method func in api.methods) {
+ final String returnTypeName = _className(options.prefix, func.returnType);
+ if (func.isAsynchronous) {
+ if (func.returnType == 'void') {
+ if (func.argType == 'void') {
+ indent.writeln(
+ '-(void)${func.name}:(void(^)(FlutterError *_Nullable))completion;');
+ } else {
+ final String argType = _className(options.prefix, func.argType);
+ indent.writeln(
+ '-(void)${func.name}:(nullable $argType *)input completion:(void(^)(FlutterError *_Nullable))completion;');
+ }
+ } else {
+ if (func.argType == 'void') {
+ indent.writeln(
+ '-(void)${func.name}:(void(^)($returnTypeName *_Nullable, FlutterError *_Nullable))completion;');
+ } else {
+ final String argType = _className(options.prefix, func.argType);
+ indent.writeln(
+ '-(void)${func.name}:(nullable $argType *)input completion:(void(^)($returnTypeName *_Nullable, FlutterError *_Nullable))completion;');
+ }
+ }
+ } else {
+ final String returnType =
+ func.returnType == 'void' ? 'void' : 'nullable $returnTypeName *';
+ if (func.argType == 'void') {
+ indent.writeln(
+ '-($returnType)${func.name}:(FlutterError *_Nullable *_Nonnull)error;');
+ } else {
+ final String argType = _className(options.prefix, func.argType);
+ indent.writeln(
+ '-($returnType)${func.name}:($argType*)input error:(FlutterError *_Nullable *_Nonnull)error;');
+ }
+ }
+ }
+ indent.writeln('@end');
+ indent.writeln('');
+ indent.writeln(
+ 'extern void ${apiName}Setup(id<FlutterBinaryMessenger> binaryMessenger, id<$apiName> _Nullable api);');
+ indent.writeln('');
+}
+
+void _writeFlutterApiDeclaration(Indent indent, Api api, ObjcOptions options) {
+ final String apiName = _className(options.prefix, api.name);
+ indent.writeln('@interface $apiName : NSObject');
+ indent.writeln(
+ '- (instancetype)initWithBinaryMessenger:(id<FlutterBinaryMessenger>)binaryMessenger;');
+ for (final Method func in api.methods) {
+ final String returnType = _className(options.prefix, func.returnType);
+ final String callbackType = _callbackForType(func.returnType, returnType);
+ if (func.argType == 'void') {
+ indent.writeln('- (void)${func.name}:($callbackType)completion;');
+ } else {
+ final String argType = _className(options.prefix, func.argType);
+ indent.writeln(
+ '- (void)${func.name}:($argType*)input completion:($callbackType)completion;');
+ }
+ }
+ indent.writeln('@end');
+}
+
+/// Generates the ".h" file for the AST represented by [root] to [sink] with the
+/// provided [options].
+void generateObjcHeader(ObjcOptions options, Root root, StringSink sink) {
+ final Indent indent = Indent(sink);
+ indent.writeln('// $generatedCodeWarning');
+ indent.writeln('// $seeAlsoWarning');
+ indent.writeln('#import <Foundation/Foundation.h>');
+ indent.writeln('@protocol FlutterBinaryMessenger;');
+ indent.writeln('@class FlutterError;');
+ indent.writeln('@class FlutterStandardTypedData;');
+ indent.writeln('');
+
+ indent.writeln('NS_ASSUME_NONNULL_BEGIN');
+ indent.writeln('');
+
+ for (final Class klass in root.classes) {
+ indent.writeln('@class ${_className(options.prefix, klass.name)};');
+ }
+
+ indent.writeln('');
+
+ _writeClassDeclarations(indent, root.classes, options.prefix);
+
+ for (final Api api in root.apis) {
+ if (api.location == ApiLocation.host) {
+ _writeHostApiDeclaration(indent, api, options);
+ } else if (api.location == ApiLocation.flutter) {
+ _writeFlutterApiDeclaration(indent, api, options);
+ }
+ }
+
+ indent.writeln('NS_ASSUME_NONNULL_END');
+}
+
+String _dictGetter(
+ List<String> classnames, String dict, Field field, String? prefix) {
+ if (classnames.contains(field.dataType)) {
+ String className = field.dataType;
+ if (prefix != null) {
+ className = '$prefix$className';
+ }
+ return '[$className fromMap:$dict[@"${field.name}"]]';
+ } else {
+ return '$dict[@"${field.name}"]';
+ }
+}
+
+String _dictValue(List<String> classnames, Field field) {
+ if (classnames.contains(field.dataType)) {
+ return '(self.${field.name} ? [self.${field.name} toMap] : [NSNull null])';
+ } else {
+ return '(self.${field.name} ? self.${field.name} : [NSNull null])';
+ }
+}
+
+void _writeHostApiSource(Indent indent, ObjcOptions options, Api api) {
+ assert(api.location == ApiLocation.host);
+ final String apiName = _className(options.prefix, api.name);
+ indent.write(
+ 'void ${apiName}Setup(id<FlutterBinaryMessenger> binaryMessenger, id<$apiName> api) ');
+ indent.scoped('{', '}', () {
+ for (final Method func in api.methods) {
+ indent.write('');
+ indent.scoped('{', '}', () {
+ indent.writeln('FlutterBasicMessageChannel *channel =');
+ indent.inc();
+ indent.writeln('[FlutterBasicMessageChannel');
+ indent.inc();
+ indent
+ .writeln('messageChannelWithName:@"${makeChannelName(api, func)}"');
+ indent.writeln('binaryMessenger:binaryMessenger];');
+ indent.dec();
+ indent.dec();
+
+ indent.write('if (api) ');
+ indent.scoped('{', '}', () {
+ indent.write(
+ '[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) ');
+ indent.scoped('{', '}];', () {
+ final String returnType =
+ _className(options.prefix, func.returnType);
+ String syncCall;
+ if (func.argType == 'void') {
+ syncCall = '[api ${func.name}:&error]';
+ } else {
+ final String argType = _className(options.prefix, func.argType);
+ indent.writeln('$argType *input = [$argType fromMap:message];');
+ syncCall = '[api ${func.name}:input error:&error]';
+ }
+ if (func.isAsynchronous) {
+ if (func.returnType == 'void') {
+ const String callback = 'callback(error);';
+ if (func.argType == 'void') {
+ indent.writeScoped(
+ '[api ${func.name}:^(FlutterError *_Nullable error) {',
+ '}];', () {
+ indent.writeln(callback);
+ });
+ } else {
+ indent.writeScoped(
+ '[api ${func.name}:input completion:^(FlutterError *_Nullable error) {',
+ '}];', () {
+ indent.writeln(callback);
+ });
+ }
+ } else {
+ const String callback =
+ 'callback(wrapResult([output toMap], error));';
+ if (func.argType == 'void') {
+ indent.writeScoped(
+ '[api ${func.name}:^($returnType *_Nullable output, FlutterError *_Nullable error) {',
+ '}];', () {
+ indent.writeln(callback);
+ });
+ } else {
+ indent.writeScoped(
+ '[api ${func.name}:input completion:^($returnType *_Nullable output, FlutterError *_Nullable error) {',
+ '}];', () {
+ indent.writeln(callback);
+ });
+ }
+ }
+ } else {
+ indent.writeln('FlutterError *error;');
+ if (func.returnType == 'void') {
+ indent.writeln('$syncCall;');
+ indent.writeln('callback(wrapResult(nil, error));');
+ } else {
+ indent.writeln('$returnType *output = $syncCall;');
+ indent.writeln('callback(wrapResult([output toMap], error));');
+ }
+ }
+ });
+ });
+ indent.write('else ');
+ indent.scoped('{', '}', () {
+ indent.writeln('[channel setMessageHandler:nil];');
+ });
+ });
+ }
+ });
+}
+
+void _writeFlutterApiSource(Indent indent, ObjcOptions options, Api api) {
+ assert(api.location == ApiLocation.flutter);
+ final String apiName = _className(options.prefix, api.name);
+ indent.writeln('@interface $apiName ()');
+ indent.writeln(
+ '@property (nonatomic, strong) NSObject<FlutterBinaryMessenger>* binaryMessenger;');
+ indent.writeln('@end');
+ indent.addln('');
+ indent.writeln('@implementation $apiName');
+ indent.write(
+ '- (instancetype)initWithBinaryMessenger:(NSObject<FlutterBinaryMessenger>*)binaryMessenger ');
+ indent.scoped('{', '}', () {
+ indent.writeln('self = [super init];');
+ indent.write('if (self) ');
+ indent.scoped('{', '}', () {
+ indent.writeln('_binaryMessenger = binaryMessenger;');
+ });
+ indent.writeln('return self;');
+ });
+ indent.addln('');
+ for (final Method func in api.methods) {
+ final String returnType = _className(options.prefix, func.returnType);
+ final String callbackType = _callbackForType(func.returnType, returnType);
+
+ String sendArgument;
+ if (func.argType == 'void') {
+ indent.write('- (void)${func.name}:($callbackType)completion ');
+ sendArgument = 'nil';
+ } else {
+ final String argType = _className(options.prefix, func.argType);
+ indent.write(
+ '- (void)${func.name}:($argType*)input completion:($callbackType)completion ');
+ sendArgument = 'inputMap';
+ }
+ indent.scoped('{', '}', () {
+ indent.writeln('FlutterBasicMessageChannel *channel =');
+ indent.inc();
+ indent.writeln('[FlutterBasicMessageChannel');
+ indent.inc();
+ indent.writeln('messageChannelWithName:@"${makeChannelName(api, func)}"');
+ indent.writeln('binaryMessenger:self.binaryMessenger];');
+ indent.dec();
+ indent.dec();
+ if (func.argType != 'void') {
+ indent.writeln('NSDictionary* inputMap = [input toMap];');
+ }
+ indent.write('[channel sendMessage:$sendArgument reply:^(id reply) ');
+ indent.scoped('{', '}];', () {
+ if (func.returnType == 'void') {
+ indent.writeln('completion(nil);');
+ } else {
+ indent.writeln('NSDictionary* outputMap = reply;');
+ indent.writeln(
+ '$returnType * output = [$returnType fromMap:outputMap];');
+ indent.writeln('completion(output, nil);');
+ }
+ });
+ });
+ }
+ indent.writeln('@end');
+}
+
+/// Generates the ".m" file for the AST represented by [root] to [sink] with the
+/// provided [options].
+void generateObjcSource(ObjcOptions options, Root root, StringSink sink) {
+ final Indent indent = Indent(sink);
+ final List<String> classnames =
+ root.classes.map((Class x) => x.name).toList();
+
+ indent.writeln('// $generatedCodeWarning');
+ indent.writeln('// $seeAlsoWarning');
+ indent.writeln('#import "${options.header}"');
+ indent.writeln('#import <Flutter/Flutter.h>');
+ indent.writeln('');
+
+ indent.writeln('#if !__has_feature(objc_arc)');
+ indent.writeln('#error File requires ARC to be enabled.');
+ indent.writeln('#endif');
+ indent.addln('');
+
+ indent.format('''
+static NSDictionary<NSString*, id>* wrapResult(NSDictionary *result, FlutterError *error) {
+\tNSDictionary *errorDict = (NSDictionary *)[NSNull null];
+\tif (error) {
+\t\terrorDict = @{
+\t\t\t\t@"${Keys.errorCode}": (error.code ? error.code : [NSNull null]),
+\t\t\t\t@"${Keys.errorMessage}": (error.message ? error.message : [NSNull null]),
+\t\t\t\t@"${Keys.errorDetails}": (error.details ? error.details : [NSNull null]),
+\t\t\t\t};
+\t}
+\treturn @{
+\t\t\t@"${Keys.result}": (result ? result : [NSNull null]),
+\t\t\t@"${Keys.error}": errorDict,
+\t\t\t};
+}''');
+ indent.addln('');
+
+ for (final Class klass in root.classes) {
+ final String className = _className(options.prefix, klass.name);
+ indent.writeln('@interface $className ()');
+ indent.writeln('+($className*)fromMap:(NSDictionary*)dict;');
+ indent.writeln('-(NSDictionary*)toMap;');
+ indent.writeln('@end');
+ }
+
+ indent.writeln('');
+
+ for (final Class klass in root.classes) {
+ final String className = _className(options.prefix, klass.name);
+ indent.writeln('@implementation $className');
+ indent.write('+($className*)fromMap:(NSDictionary*)dict ');
+ indent.scoped('{', '}', () {
+ const String resultName = 'result';
+ indent.writeln('$className* $resultName = [[$className alloc] init];');
+ for (final Field field in klass.fields) {
+ indent.writeln(
+ '$resultName.${field.name} = ${_dictGetter(classnames, 'dict', field, options.prefix)};');
+ indent.write(
+ 'if ((NSNull *)$resultName.${field.name} == [NSNull null]) ');
+ indent.scoped('{', '}', () {
+ indent.writeln('$resultName.${field.name} = nil;');
+ });
+ }
+ indent.writeln('return $resultName;');
+ });
+ indent.write('-(NSDictionary*)toMap ');
+ indent.scoped('{', '}', () {
+ indent.write('return [NSDictionary dictionaryWithObjectsAndKeys:');
+ for (final Field field in klass.fields) {
+ indent.add(_dictValue(classnames, field) + ', @"${field.name}", ');
+ }
+ indent.addln('nil];');
+ });
+ indent.writeln('@end');
+ indent.writeln('');
+ }
+
+ for (final Api api in root.apis) {
+ if (api.location == ApiLocation.host) {
+ _writeHostApiSource(indent, options, api);
+ } else if (api.location == ApiLocation.flutter) {
+ _writeFlutterApiSource(indent, options, api);
+ }
+ }
+}
diff --git a/packages/pigeon/lib/pigeon.dart b/packages/pigeon/lib/pigeon.dart
new file mode 100644
index 0000000..5f19495
--- /dev/null
+++ b/packages/pigeon/lib/pigeon.dart
@@ -0,0 +1,2 @@
+export 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List;
+export 'pigeon_lib.dart';
diff --git a/packages/pigeon/lib/pigeon_cl.dart b/packages/pigeon/lib/pigeon_cl.dart
new file mode 100644
index 0000000..096c482
--- /dev/null
+++ b/packages/pigeon/lib/pigeon_cl.dart
@@ -0,0 +1,75 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:path/path.dart' as path;
+import 'package:pigeon/pigeon_lib.dart';
+
+/// This creates a relative path from `from` to `input`, the output being a
+/// posix path on all platforms.
+String _posixRelative(String input, {required String from}) {
+ final path.Context context = path.Context(style: path.Style.posix);
+ final String rawInputPath = input;
+ final String absInputPath = File(rawInputPath).absolute.path;
+ // By going through URI's we can make sure paths can go between drives in
+ // Windows.
+ final Uri inputUri = path.toUri(absInputPath);
+ final String posixAbsInputPath = context.fromUri(inputUri);
+ final Uri tempUri = path.toUri(from);
+ final String posixTempPath = context.fromUri(tempUri);
+ return context.relative(posixAbsInputPath, from: posixTempPath);
+}
+
+/// This is the main entrypoint for the command-line tool. [args] are the
+/// commmand line arguments and there is an optional [packageConfig] to
+/// accomodate users that want to integrate pigeon with other build systems.
+Future<int> runCommandLine(List<String> args, {Uri? packageConfig}) async {
+ final PigeonOptions opts = Pigeon.parseArgs(args);
+ final Directory tempDir = Directory.systemTemp.createTempSync(
+ 'flutter_pigeon.',
+ );
+
+ String importLine = '';
+ if (opts.input != null) {
+ final String relInputPath = _posixRelative(opts.input!, from: tempDir.path);
+ importLine = 'import \'$relInputPath\';\n';
+ }
+ final String code = """
+// @dart = 2.12
+$importLine
+import 'dart:io';
+import 'dart:isolate';
+import 'package:pigeon/pigeon_lib.dart';
+void main(List<String> args, SendPort sendPort) async {
+ sendPort.send(await Pigeon.run(args));
+}
+""";
+ final File tempFile = File(path.join(tempDir.path, '_pigeon_temp_.dart'));
+ await tempFile.writeAsString(code);
+ final ReceivePort receivePort = ReceivePort();
+ Isolate.spawnUri(
+ // Using Uri.file instead of Uri.parse in order to parse backslashes as
+ // path segment separator with Windows semantics.
+ Uri.file(tempFile.path),
+ args,
+ receivePort.sendPort,
+ packageConfig: packageConfig,
+ );
+
+ final Completer<int> completer = Completer<int>();
+ receivePort.listen((dynamic message) {
+ try {
+ // ignore: avoid_as
+ completer.complete(message as int);
+ } catch (exception) {
+ completer.completeError(exception);
+ }
+ });
+ final int exitCode = await completer.future;
+ tempDir.deleteSync(recursive: true);
+ return exitCode;
+}
diff --git a/packages/pigeon/lib/pigeon_lib.dart b/packages/pigeon/lib/pigeon_lib.dart
new file mode 100644
index 0000000..5228bb9
--- /dev/null
+++ b/packages/pigeon/lib/pigeon_lib.dart
@@ -0,0 +1,518 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+import 'dart:mirrors';
+
+import 'package:args/args.dart';
+import 'package:path/path.dart' as path;
+import 'package:path/path.dart';
+import 'package:pigeon/java_generator.dart';
+
+import 'ast.dart';
+import 'dart_generator.dart';
+import 'generator_tools.dart';
+import 'objc_generator.dart';
+
+const List<String> _validTypes = <String>[
+ 'String',
+ 'bool',
+ 'int',
+ 'double',
+ 'Uint8List',
+ 'Int32List',
+ 'Int64List',
+ 'Float64List',
+ 'List',
+ 'Map',
+];
+
+class _Asynchronous {
+ const _Asynchronous();
+}
+
+/// Metadata to annotate a Api method as asynchronous
+const _Asynchronous async = _Asynchronous();
+
+/// Metadata to annotate a Pigeon API implemented by the host-platform.
+///
+/// The abstract class with this annotation groups a collection of Dart↔host
+/// interop methods. These methods are invoked by Dart and are received by a
+/// host-platform (such as in Android or iOS) by a class implementing the
+/// generated host-platform interface.
+class HostApi {
+ /// Parametric constructor for [HostApi].
+ const HostApi({this.dartHostTestHandler});
+
+ /// The name of an interface generated for tests. Implement this
+ /// interface and invoke `[name of this handler].setup` to receive
+ /// calls from your real [HostApi] class in Dart instead of the host
+ /// platform code, as is typical.
+ ///
+ /// When using this, you must specify the `--out_test_dart` argument
+ /// to specify where to generate the test file.
+ ///
+ /// Prefer to use a mock of the real [HostApi] with a mocking library for unit
+ /// tests. Generating this Dart handler is sometimes useful in integration
+ /// testing.
+ ///
+ /// Defaults to `null` in which case no handler will be generated.
+ final String? dartHostTestHandler;
+}
+
+/// Metadata to annotate a Pigeon API implemented by Flutter.
+///
+/// The abstract class with this annotation groups a collection of Dart↔host
+/// interop methods. These methods are invoked by the host-platform (such as in
+/// Android or iOS) and are received by Flutter by a class implementing the
+/// generated Dart interface.
+class FlutterApi {
+ /// Parametric constructor for [FlutterApi].
+ const FlutterApi();
+}
+
+/// Represents an error as a result of parsing and generating code.
+class Error {
+ /// Parametric constructor for Error.
+ Error({
+ required this.message,
+ this.filename,
+ this.lineNumber,
+ });
+
+ /// A description of the error.
+ String message;
+
+ /// What file caused the [Error].
+ String? filename;
+
+ /// What line the error happened on.
+ int? lineNumber;
+
+ @override
+ String toString() {
+ return '(Error message:"$message" filename:"$filename" lineNumber:$lineNumber)';
+ }
+}
+
+bool _isApi(ClassMirror classMirror) {
+ return classMirror.isAbstract &&
+ (_getHostApi(classMirror) != null || _isFlutterApi(classMirror));
+}
+
+HostApi? _getHostApi(ClassMirror apiMirror) {
+ for (final InstanceMirror instance in apiMirror.metadata) {
+ if (instance.reflectee is HostApi) {
+ return instance.reflectee;
+ }
+ }
+ return null;
+}
+
+bool _isFlutterApi(ClassMirror apiMirror) {
+ for (final InstanceMirror instance in apiMirror.metadata) {
+ if (instance.reflectee is FlutterApi) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/// Options used when running the code generator.
+class PigeonOptions {
+ /// Creates a instance of PigeonOptions
+ PigeonOptions();
+
+ /// Path to the file which will be processed.
+ String? input;
+
+ /// Path to the dart file that will be generated.
+ String? dartOut;
+
+ /// Path to the dart file that will be generated for test support classes.
+ String? dartTestOut;
+
+ /// Path to the ".h" Objective-C file will be generated.
+ String? objcHeaderOut;
+
+ /// Path to the ".m" Objective-C file will be generated.
+ String? objcSourceOut;
+
+ /// Options that control how Objective-C will be generated.
+ ObjcOptions? objcOptions;
+
+ /// Path to the java file that will be generated.
+ String? javaOut;
+
+ /// Options that control how Java will be generated.
+ JavaOptions? javaOptions;
+
+ /// Options that control how Dart will be generated.
+ DartOptions? dartOptions = DartOptions();
+}
+
+/// A collection of an AST represented as a [Root] and [Error]'s.
+class ParseResults {
+ /// Parametric constructor for [ParseResults].
+ ParseResults({
+ required this.root,
+ required this.errors,
+ });
+
+ /// The resulting AST.
+ final Root root;
+
+ /// Errors generated while parsing input.
+ final List<Error> errors;
+}
+
+/// Tool for generating code to facilitate platform channels usage.
+class Pigeon {
+ /// Create and setup a [Pigeon] instance.
+ static Pigeon setup() {
+ return Pigeon();
+ }
+
+ Class _parseClassMirror(ClassMirror klassMirror) {
+ final List<Field> fields = <Field>[];
+ for (final DeclarationMirror declaration
+ in klassMirror.declarations.values) {
+ if (declaration is VariableMirror) {
+ fields.add(Field(
+ name: MirrorSystem.getName(declaration.simpleName),
+ dataType: MirrorSystem.getName(
+ declaration.type.simpleName,
+ ),
+ ));
+ }
+ }
+ final Class klass = Class(
+ name: MirrorSystem.getName(klassMirror.simpleName),
+ fields: fields,
+ );
+ return klass;
+ }
+
+ Iterable<Class> _parseClassMirrors(Iterable<ClassMirror> mirrors) sync* {
+ for (final ClassMirror mirror in mirrors) {
+ yield _parseClassMirror(mirror);
+ final Iterable<ClassMirror> nestedTypes = mirror.declarations.values
+ .whereType<VariableMirror>()
+ .map((VariableMirror variable) => variable.type)
+ .whereType<ClassMirror>()
+
+ ///note: This will need to be changed if we support generic types.
+ .where((ClassMirror mirror) =>
+ !_validTypes.contains(MirrorSystem.getName(mirror.simpleName)));
+ for (final Class klass in _parseClassMirrors(nestedTypes)) {
+ yield klass;
+ }
+ }
+ }
+
+ Iterable<T> _unique<T, U>(Iterable<T> iter, U Function(T val) getKey) sync* {
+ final Set<U> seen = <U>{};
+ for (final T val in iter) {
+ if (seen.add(getKey(val))) {
+ yield val;
+ }
+ }
+ }
+
+ /// Use reflection to parse the [types] provided.
+ ParseResults parse(List<Type> types) {
+ final Set<ClassMirror> classes = <ClassMirror>{};
+ final List<ClassMirror> apis = <ClassMirror>[];
+
+ for (final Type type in types) {
+ final ClassMirror classMirror = reflectClass(type);
+ if (_isApi(classMirror)) {
+ apis.add(classMirror);
+ } else {
+ classes.add(classMirror);
+ }
+ }
+
+ for (final ClassMirror apiMirror in apis) {
+ for (final DeclarationMirror declaration
+ in apiMirror.declarations.values) {
+ if (declaration is MethodMirror && !declaration.isConstructor) {
+ if (!isVoid(declaration.returnType)) {
+ classes.add(declaration.returnType as ClassMirror);
+ }
+ if (declaration.parameters.isNotEmpty) {
+ classes.add(declaration.parameters[0].type as ClassMirror);
+ }
+ }
+ }
+ }
+ final Root root = Root(
+ classes:
+ _unique(_parseClassMirrors(classes), (Class x) => x.name).toList(),
+ apis: <Api>[],
+ );
+ for (final ClassMirror apiMirror in apis) {
+ final List<Method> functions = <Method>[];
+ for (final DeclarationMirror declaration
+ in apiMirror.declarations.values) {
+ if (declaration is MethodMirror && !declaration.isConstructor) {
+ final bool isAsynchronous =
+ declaration.metadata.any((InstanceMirror it) {
+ return MirrorSystem.getName(it.type.simpleName) ==
+ '${async.runtimeType}';
+ });
+ functions.add(Method(
+ name: MirrorSystem.getName(declaration.simpleName),
+ argType: declaration.parameters.isEmpty
+ ? 'void'
+ : MirrorSystem.getName(
+ declaration.parameters[0].type.simpleName),
+ returnType: MirrorSystem.getName(declaration.returnType.simpleName),
+ isAsynchronous: isAsynchronous,
+ ));
+ }
+ }
+ final HostApi? hostApi = _getHostApi(apiMirror);
+ root.apis.add(Api(
+ name: MirrorSystem.getName(apiMirror.simpleName),
+ location: hostApi != null ? ApiLocation.host : ApiLocation.flutter,
+ methods: functions,
+ dartHostTestHandler: hostApi?.dartHostTestHandler,
+ ));
+ }
+
+ final List<Error> validateErrors = _validateAst(root);
+ return ParseResults(root: root, errors: validateErrors);
+ }
+
+ /// String that describes how the tool is used.
+ static String get usage {
+ return '''
+
+Pigeon is a tool for generating type-safe communication code between Flutter
+and the host platform.
+
+usage: pigeon --input <pigeon path> --dart_out <dart path> [option]*
+
+options:
+''' +
+ _argParser.usage;
+ }
+
+ static final ArgParser _argParser = ArgParser()
+ ..addOption('input', help: 'REQUIRED: Path to pigeon file.')
+ ..addOption('dart_out',
+ help: 'REQUIRED: Path to generated Dart source file (.dart).')
+ ..addOption('dart_test_out',
+ help: 'Path to generated library for Dart tests, when using '
+ '@HostApi(dartHostTestHandler:).')
+ ..addOption('objc_source_out',
+ help: 'Path to generated Objective-C source file (.m).')
+ ..addOption('java_out', help: 'Path to generated Java file (.java).')
+ ..addOption('java_package',
+ help: 'The package that generated Java code will be in.')
+ ..addFlag('dart_null_safety',
+ help: 'Makes generated Dart code have null safety annotations',
+ defaultsTo: true)
+ ..addOption('objc_header_out',
+ help: 'Path to generated Objective-C header file (.h).')
+ ..addOption('objc_prefix',
+ help: 'Prefix for generated Objective-C classes and protocols.');
+
+ /// Convert command-line arguments to [PigeonOptions].
+ static PigeonOptions parseArgs(List<String> args) {
+ // Note: This function shouldn't perform any logic, just translate the args
+ // to PigeonOptions. Synthesized values inside of the PigeonOption should
+ // get set in the `run` function to accomodate users that are using the
+ // `configurePigeon` function.
+ final ArgResults results = _argParser.parse(args);
+
+ final PigeonOptions opts = PigeonOptions();
+ opts.input = results['input'];
+ opts.dartOut = results['dart_out'];
+ opts.dartTestOut = results['dart_test_out'];
+ opts.objcHeaderOut = results['objc_header_out'];
+ opts.objcSourceOut = results['objc_source_out'];
+ opts.objcOptions = ObjcOptions(
+ prefix: results['objc_prefix'],
+ );
+ opts.javaOut = results['java_out'];
+ opts.javaOptions = JavaOptions(
+ package: results['java_package'],
+ );
+ opts.dartOptions = DartOptions()..isNullSafe = results['dart_null_safety'];
+ return opts;
+ }
+
+ static Future<void> _runGenerator(
+ String output, void Function(IOSink sink) func) async {
+ IOSink sink;
+ File file;
+ if (output == 'stdout') {
+ sink = stdout;
+ } else {
+ file = File(output);
+ sink = file.openWrite();
+ }
+ func(sink);
+ await sink.flush();
+ }
+
+ List<Error> _validateAst(Root root) {
+ final List<Error> result = <Error>[];
+ final List<String> customClasses =
+ root.classes.map((Class x) => x.name).toList();
+ for (final Class klass in root.classes) {
+ for (final Field field in klass.fields) {
+ if (!(_validTypes.contains(field.dataType) ||
+ customClasses.contains(field.dataType))) {
+ result.add(Error(
+ message:
+ 'Unsupported datatype:"${field.dataType}" in class "${klass.name}".'));
+ }
+ }
+ }
+ for (final Api api in root.apis) {
+ for (final Method method in api.methods) {
+ if (_validTypes.contains(method.argType)) {
+ result.add(Error(
+ message:
+ 'Unsupported argument type: "${method.argType}" in API: "${api.name}" method: "${method.name}'));
+ }
+ if (_validTypes.contains(method.returnType)) {
+ result.add(Error(
+ message:
+ 'Unsupported return type: "${method.returnType}" in API: "${api.name}" method: "${method.name}'));
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /// Crawls through the reflection system looking for a configurePigeon method and
+ /// executing it.
+ static void _executeConfigurePigeon(PigeonOptions options) {
+ for (final LibraryMirror library
+ in currentMirrorSystem().libraries.values) {
+ for (final DeclarationMirror declaration in library.declarations.values) {
+ if (declaration is MethodMirror &&
+ MirrorSystem.getName(declaration.simpleName) == 'configurePigeon') {
+ if (declaration.parameters.length == 1 &&
+ declaration.parameters[0].type == reflectClass(PigeonOptions)) {
+ library.invoke(declaration.simpleName, <dynamic>[options]);
+ } else {
+ print('warning: invalid \'configurePigeon\' method defined.');
+ }
+ }
+ }
+ }
+ }
+
+ static String _posixify(String input) {
+ final path.Context context = path.Context(style: path.Style.posix);
+ return context.fromUri(path.toUri(path.absolute(input)));
+ }
+
+ /// The 'main' entrypoint used by the command-line tool. [args] are the
+ /// command-line arguments.
+ static Future<int> run(List<String> args) async {
+ final Pigeon pigeon = Pigeon.setup();
+ final PigeonOptions options = Pigeon.parseArgs(args);
+
+ _executeConfigurePigeon(options);
+
+ if (options.input == null || options.dartOut == null) {
+ print(usage);
+ return 0;
+ }
+
+ final List<Error> errors = <Error>[];
+ final List<Type> apis = <Type>[];
+ if (options.objcHeaderOut != null) {
+ options.objcOptions?.header = basename(options.objcHeaderOut!);
+ }
+
+ for (final LibraryMirror library
+ in currentMirrorSystem().libraries.values) {
+ for (final DeclarationMirror declaration in library.declarations.values) {
+ if (declaration is ClassMirror && _isApi(declaration)) {
+ apis.add(declaration.reflectedType);
+ }
+ }
+ }
+
+ if (apis.isNotEmpty) {
+ final ParseResults parseResults = pigeon.parse(apis);
+ for (final Error err in parseResults.errors) {
+ errors.add(Error(message: err.message, filename: options.input));
+ }
+ if (options.dartOut != null) {
+ await _runGenerator(
+ options.dartOut!,
+ (StringSink sink) => generateDart(
+ options.dartOptions ?? DartOptions(), parseResults.root, sink));
+ }
+ if (options.dartTestOut != null && options.dartOut != null) {
+ final String mainPath = context.relative(
+ _posixify(options.dartOut!),
+ from: _posixify(path.dirname(options.dartTestOut!)),
+ );
+ await _runGenerator(
+ options.dartTestOut!,
+ (StringSink sink) => generateTestDart(
+ options.dartOptions ?? DartOptions(),
+ parseResults.root,
+ sink,
+ mainPath,
+ ),
+ );
+ }
+ if (options.objcHeaderOut != null) {
+ await _runGenerator(
+ options.objcHeaderOut!,
+ (StringSink sink) => generateObjcHeader(
+ options.objcOptions ?? ObjcOptions(), parseResults.root, sink));
+ }
+ if (options.objcSourceOut != null) {
+ await _runGenerator(
+ options.objcSourceOut!,
+ (StringSink sink) => generateObjcSource(
+ options.objcOptions ?? ObjcOptions(), parseResults.root, sink));
+ }
+ if (options.javaOut != null) {
+ if (options.javaOptions!.className == null) {
+ options.javaOptions!.className =
+ path.basenameWithoutExtension(options.javaOut!);
+ }
+ await _runGenerator(
+ options.javaOut!,
+ (StringSink sink) => generateJava(
+ options.javaOptions ?? JavaOptions(), parseResults.root, sink));
+ }
+ } else {
+ errors.add(Error(message: 'No pigeon classes found, nothing generated.'));
+ }
+
+ printErrors(errors);
+
+ return errors.isNotEmpty ? 1 : 0;
+ }
+
+ /// Print a list of errors to stderr.
+ static void printErrors(List<Error> errors) {
+ for (final Error err in errors) {
+ if (err.filename != null) {
+ if (err.lineNumber != null) {
+ stderr.writeln(
+ 'Error: ${err.filename}:${err.lineNumber}: ${err.message}');
+ } else {
+ stderr.writeln('Error: ${err.filename}: ${err.message}');
+ }
+ } else {
+ stderr.writeln('Error: ${err.message}');
+ }
+ }
+ }
+}
diff --git a/packages/pigeon/mock_handler_tester/.gitignore b/packages/pigeon/mock_handler_tester/.gitignore
new file mode 100644
index 0000000..f3c2053
--- /dev/null
+++ b/packages/pigeon/mock_handler_tester/.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/pigeon/mock_handler_tester/.metadata b/packages/pigeon/mock_handler_tester/.metadata
new file mode 100644
index 0000000..bb4f09a
--- /dev/null
+++ b/packages/pigeon/mock_handler_tester/.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: e6b697a9df5e9ce933024be3334e86b599c60e71
+ channel: unknown
+
+project_type: app
diff --git a/packages/pigeon/mock_handler_tester/README.md b/packages/pigeon/mock_handler_tester/README.md
new file mode 100644
index 0000000..698b506
--- /dev/null
+++ b/packages/pigeon/mock_handler_tester/README.md
@@ -0,0 +1,3 @@
+# mock_handler_tester
+
+A test bed for testing the code generated by `dartHostTestHandler`.
diff --git a/packages/pigeon/mock_handler_tester/lib/main.dart b/packages/pigeon/mock_handler_tester/lib/main.dart
new file mode 100644
index 0000000..e4b5c93
--- /dev/null
+++ b/packages/pigeon/mock_handler_tester/lib/main.dart
@@ -0,0 +1,5 @@
+import 'package:flutter/material.dart';
+
+void main() {
+ runApp(Container());
+}
diff --git a/packages/pigeon/mock_handler_tester/pubspec.yaml b/packages/pigeon/mock_handler_tester/pubspec.yaml
new file mode 100644
index 0000000..1826c25
--- /dev/null
+++ b/packages/pigeon/mock_handler_tester/pubspec.yaml
@@ -0,0 +1,19 @@
+name: mock_handler_tester
+description: A testbed for testing dartHostTestHandler.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+version: 1.0.0+1
+
+environment:
+ sdk: ">=2.12.0 <3.0.0"
+
+dependencies:
+ cupertino_icons: ^1.0.2
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+flutter:
+ uses-material-design: true
diff --git a/packages/pigeon/mock_handler_tester/test/message.dart b/packages/pigeon/mock_handler_tester/test/message.dart
new file mode 100644
index 0000000..0896b04
--- /dev/null
+++ b/packages/pigeon/mock_handler_tester/test/message.dart
@@ -0,0 +1,166 @@
+// Autogenerated from Pigeon (v0.2.0-nullsafety.0), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types
+// @dart = 2.12
+import 'dart:async';
+import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List;
+
+import 'package:flutter/services.dart';
+
+class SearchReply {
+ String? result;
+ String? error;
+
+ Object encode() {
+ final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+ pigeonMap['result'] = result;
+ pigeonMap['error'] = error;
+ return pigeonMap;
+ }
+
+ static SearchReply decode(Object message) {
+ final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+ return SearchReply()
+ ..result = pigeonMap['result'] as String?
+ ..error = pigeonMap['error'] as String?;
+ }
+}
+
+class SearchRequest {
+ String? query;
+ int? anInt;
+ bool? aBool;
+
+ Object encode() {
+ final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+ pigeonMap['query'] = query;
+ pigeonMap['anInt'] = anInt;
+ pigeonMap['aBool'] = aBool;
+ return pigeonMap;
+ }
+
+ static SearchRequest decode(Object message) {
+ final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+ return SearchRequest()
+ ..query = pigeonMap['query'] as String?
+ ..anInt = pigeonMap['anInt'] as int?
+ ..aBool = pigeonMap['aBool'] as bool?;
+ }
+}
+
+class Nested {
+ SearchRequest? request;
+
+ Object encode() {
+ final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+ pigeonMap['request'] = request == null ? null : request!.encode();
+ return pigeonMap;
+ }
+
+ static Nested decode(Object message) {
+ final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+ return Nested()
+ ..request = pigeonMap['request'] != null
+ ? SearchRequest.decode(pigeonMap['request']!)
+ : null;
+ }
+}
+
+abstract class FlutterSearchApi {
+ SearchReply search(SearchRequest arg);
+ static void setup(FlutterSearchApi? api) {
+ {
+ const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.FlutterSearchApi.search', StandardMessageCodec());
+ if (api == null) {
+ channel.setMessageHandler(null);
+ } else {
+ channel.setMessageHandler((Object? message) async {
+ assert(message != null,
+ 'Argument for dev.flutter.pigeon.FlutterSearchApi.search was null. Expected SearchRequest.');
+ final SearchRequest input = SearchRequest.decode(message!);
+ final SearchReply output = api.search(input);
+ return output.encode();
+ });
+ }
+ }
+ }
+}
+
+class NestedApi {
+ Future<SearchReply> search(Nested arg) async {
+ final Object encoded = arg.encode();
+ const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.NestedApi.search', StandardMessageCodec());
+ final Map<Object?, Object?>? replyMap =
+ await channel.send(encoded) as Map<Object?, Object?>?;
+ if (replyMap == null) {
+ throw PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel.',
+ details: null,
+ );
+ } else if (replyMap['error'] != null) {
+ final Map<Object?, Object?> error =
+ (replyMap['error'] as Map<Object?, Object?>?)!;
+ throw PlatformException(
+ code: (error['code'] as String?)!,
+ message: error['message'] as String?,
+ details: error['details'],
+ );
+ } else {
+ return SearchReply.decode(replyMap['result']!);
+ }
+ }
+}
+
+class Api {
+ Future<void> initialize() async {
+ const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.Api.initialize', StandardMessageCodec());
+ final Map<Object?, Object?>? replyMap =
+ await channel.send(null) as Map<Object?, Object?>?;
+ if (replyMap == null) {
+ throw PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel.',
+ details: null,
+ );
+ } else if (replyMap['error'] != null) {
+ final Map<Object?, Object?> error =
+ (replyMap['error'] as Map<Object?, Object?>?)!;
+ throw PlatformException(
+ code: (error['code'] as String?)!,
+ message: error['message'] as String?,
+ details: error['details'],
+ );
+ } else {
+ // noop
+ }
+ }
+
+ Future<SearchReply> search(SearchRequest arg) async {
+ final Object encoded = arg.encode();
+ const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.Api.search', StandardMessageCodec());
+ final Map<Object?, Object?>? replyMap =
+ await channel.send(encoded) as Map<Object?, Object?>?;
+ if (replyMap == null) {
+ throw PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel.',
+ details: null,
+ );
+ } else if (replyMap['error'] != null) {
+ final Map<Object?, Object?> error =
+ (replyMap['error'] as Map<Object?, Object?>?)!;
+ throw PlatformException(
+ code: (error['code'] as String?)!,
+ message: error['message'] as String?,
+ details: error['details'],
+ );
+ } else {
+ return SearchReply.decode(replyMap['result']!);
+ }
+ }
+}
diff --git a/packages/pigeon/mock_handler_tester/test/test.dart b/packages/pigeon/mock_handler_tester/test/test.dart
new file mode 100644
index 0000000..0d8e4c3
--- /dev/null
+++ b/packages/pigeon/mock_handler_tester/test/test.dart
@@ -0,0 +1,66 @@
+// Autogenerated from Pigeon (v0.2.0-nullsafety.0), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import
+// @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 'message.dart';
+
+abstract class TestNestedApi {
+ SearchReply search(Nested arg);
+ static void setup(TestNestedApi? api) {
+ {
+ const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.NestedApi.search', StandardMessageCodec());
+ if (api == null) {
+ channel.setMockMessageHandler(null);
+ } else {
+ channel.setMockMessageHandler((Object? message) async {
+ assert(message != null,
+ 'Argument for dev.flutter.pigeon.NestedApi.search was null. Expected Nested.');
+ final Nested input = Nested.decode(message!);
+ final SearchReply output = api.search(input);
+ return <Object?, Object?>{'result': output.encode()};
+ });
+ }
+ }
+ }
+}
+
+abstract class TestHostApi {
+ void initialize();
+ SearchReply search(SearchRequest arg);
+ static void setup(TestHostApi? api) {
+ {
+ const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.Api.initialize', StandardMessageCodec());
+ if (api == null) {
+ channel.setMockMessageHandler(null);
+ } else {
+ channel.setMockMessageHandler((Object? message) async {
+ // ignore message
+ api.initialize();
+ return <Object?, Object?>{};
+ });
+ }
+ }
+ {
+ const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.Api.search', StandardMessageCodec());
+ if (api == null) {
+ channel.setMockMessageHandler(null);
+ } else {
+ channel.setMockMessageHandler((Object? message) async {
+ assert(message != null,
+ 'Argument for dev.flutter.pigeon.Api.search was null. Expected SearchRequest.');
+ final SearchRequest input = SearchRequest.decode(message!);
+ final SearchReply output = api.search(input);
+ return <Object?, Object?>{'result': output.encode()};
+ });
+ }
+ }
+ }
+}
diff --git a/packages/pigeon/mock_handler_tester/test/widget_test.dart b/packages/pigeon/mock_handler_tester/test/widget_test.dart
new file mode 100644
index 0000000..0334051
--- /dev/null
+++ b/packages/pigeon/mock_handler_tester/test/widget_test.dart
@@ -0,0 +1,102 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'message.dart';
+import 'test.dart';
+
+class Mock implements TestHostApi {
+ List<String> log = <String>[];
+
+ @override
+ void initialize() {
+ log.add('initialize');
+ }
+
+ @override
+ SearchReply search(SearchRequest arg) {
+ log.add('search');
+ return SearchReply()..result = arg.query;
+ }
+}
+
+class MockNested implements TestNestedApi {
+ bool didCall = false;
+ @override
+ SearchReply search(Nested arg) {
+ didCall = true;
+ if (arg.request == null) {
+ return SearchReply();
+ } else {
+ return SearchReply()..result = arg.request?.query;
+ }
+ }
+}
+
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ test('simple', () async {
+ final NestedApi api = NestedApi();
+ final MockNested mock = MockNested();
+ TestNestedApi.setup(mock);
+ final SearchReply reply = await api.search(Nested()..request = null);
+ expect(mock.didCall, true);
+ expect(reply.result, null);
+ });
+
+ test('nested', () async {
+ final Api api = Api();
+ final Mock mock = Mock();
+ TestHostApi.setup(mock);
+ final SearchReply reply = await api.search(SearchRequest()..query = 'foo');
+ expect(mock.log, <String>['search']);
+ expect(reply.result, 'foo');
+ });
+
+ test('no-arg calls', () async {
+ final Api api = Api();
+ final Mock mock = Mock();
+ TestHostApi.setup(mock);
+ await api.initialize();
+ expect(mock.log, <String>['initialize']);
+ });
+
+ test(
+ 'calling methods with null',
+ () async {
+ final Mock mock = Mock();
+ TestHostApi.setup(mock);
+ expect(
+ await const BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.Api.initialize',
+ StandardMessageCodec(),
+ ).send(null),
+ isEmpty,
+ );
+ try {
+ await const BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.Api.search',
+ StandardMessageCodec(),
+ ).send(null) as Map<Object?, Object?>?;
+ expect(true, isFalse); // should not reach here
+ } catch (error) {
+ expect(error, isAssertionError);
+ expect(
+ error.toString(),
+ contains(
+ 'Argument for dev.flutter.pigeon.Api.search was null. Expected SearchRequest.',
+ ),
+ );
+ }
+ expect(mock.log, <String>['initialize']);
+ },
+ // TODO(ianh): skip can be removed after first stable release in 2021
+ skip: Platform.environment['CHANNEL'] == 'stable',
+ );
+}
diff --git a/packages/pigeon/pigeons/all_datatypes.dart b/packages/pigeon/pigeons/all_datatypes.dart
new file mode 100644
index 0000000..95a2886
--- /dev/null
+++ b/packages/pigeon/pigeons/all_datatypes.dart
@@ -0,0 +1,30 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class Everything {
+ bool? aBool;
+ int? anInt;
+ double? aDouble;
+ String? aString;
+ Uint8List? aByteArray;
+ Int32List? a4ByteArray;
+ Int64List? a8ByteArray;
+ Float64List? aFloatArray;
+ // ignore: always_specify_types
+ List? aList;
+ // ignore: always_specify_types
+ Map? aMap;
+}
+
+@HostApi()
+abstract class HostEverything {
+ Everything giveMeEverything();
+}
+
+@FlutterApi()
+abstract class FlutterEverything {
+ Everything giveMeEverything();
+}
diff --git a/packages/pigeon/pigeons/android_unittests.dart b/packages/pigeon/pigeons/android_unittests.dart
new file mode 100644
index 0000000..baaa69b
--- /dev/null
+++ b/packages/pigeon/pigeons/android_unittests.dart
@@ -0,0 +1,24 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class SetRequest {
+ int? value;
+}
+
+class NestedRequest {
+ String? context;
+ SetRequest? request;
+}
+
+@HostApi()
+abstract class Api {
+ void setValue(SetRequest request);
+}
+
+@HostApi()
+abstract class NestedApi {
+ void setValueWithContext(NestedRequest request);
+}
diff --git a/packages/pigeon/pigeons/async_handlers.dart b/packages/pigeon/pigeons/async_handlers.dart
new file mode 100644
index 0000000..6e2cb02
--- /dev/null
+++ b/packages/pigeon/pigeons/async_handlers.dart
@@ -0,0 +1,23 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class Value {
+ int? number;
+}
+
+@HostApi()
+abstract class Api2Host {
+ @async
+ Value calculate(Value value);
+ @async
+ void voidVoid();
+}
+
+@FlutterApi()
+abstract class Api2Flutter {
+ @async
+ Value calculate(Value value);
+}
diff --git a/packages/pigeon/pigeons/flutter_unittests.dart b/packages/pigeon/pigeons/flutter_unittests.dart
new file mode 100644
index 0000000..acc28b8
--- /dev/null
+++ b/packages/pigeon/pigeons/flutter_unittests.dart
@@ -0,0 +1,30 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class SearchRequest {
+ String? query;
+}
+
+class SearchReply {
+ String? result;
+ String? error;
+}
+
+class SearchRequests {
+ // ignore: always_specify_types
+ List? requests;
+}
+
+class SearchReplies {
+ // ignore: always_specify_types
+ List? replies;
+}
+
+@HostApi()
+abstract class Api {
+ SearchReply search(SearchRequest request);
+ SearchReplies doSearches(SearchRequests request);
+}
diff --git a/packages/pigeon/pigeons/host2flutter.dart b/packages/pigeon/pigeons/host2flutter.dart
new file mode 100644
index 0000000..5a45975
--- /dev/null
+++ b/packages/pigeon/pigeons/host2flutter.dart
@@ -0,0 +1,24 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/objc_generator.dart';
+import 'package:pigeon/pigeon.dart';
+
+class SearchRequest {
+ String? query;
+}
+
+class SearchReply {
+ String? result;
+}
+
+@FlutterApi()
+abstract class Api {
+ SearchReply search(SearchRequest request);
+}
+
+void configurePigeon(PigeonOptions options) {
+ options.objcOptions ??= ObjcOptions();
+ options.objcOptions?.prefix = 'AC';
+}
diff --git a/packages/pigeon/pigeons/java_double_host_api.dart b/packages/pigeon/pigeons/java_double_host_api.dart
new file mode 100644
index 0000000..b220947
--- /dev/null
+++ b/packages/pigeon/pigeons/java_double_host_api.dart
@@ -0,0 +1,17 @@
+import 'package:pigeon/pigeon.dart';
+
+class Response {
+ int? result;
+}
+
+@HostApi()
+abstract class BridgeApi1 {
+ @async
+ Response call();
+}
+
+@HostApi()
+abstract class BridgeApi2 {
+ @async
+ Response call();
+}
diff --git a/packages/pigeon/pigeons/list.dart b/packages/pigeon/pigeons/list.dart
new file mode 100644
index 0000000..e370ae8
--- /dev/null
+++ b/packages/pigeon/pigeons/list.dart
@@ -0,0 +1,15 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class TestMessage {
+ // ignore: always_specify_types
+ List? testList;
+}
+
+@HostApi()
+abstract class TestApi {
+ void test(TestMessage msg);
+}
diff --git a/packages/pigeon/pigeons/message.dart b/packages/pigeon/pigeons/message.dart
new file mode 100644
index 0000000..449debf
--- /dev/null
+++ b/packages/pigeon/pigeons/message.dart
@@ -0,0 +1,48 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This file is an example pigeon file that is used in compilation, unit, mock
+// handler, and e2e tests.
+
+import 'package:pigeon/java_generator.dart';
+import 'package:pigeon/objc_generator.dart';
+import 'package:pigeon/pigeon.dart';
+
+class SearchRequest {
+ String? query;
+ int? anInt;
+ bool? aBool;
+}
+
+class SearchReply {
+ String? result;
+ String? error;
+}
+
+@HostApi(dartHostTestHandler: 'TestHostApi')
+abstract class Api {
+ void initialize();
+ SearchReply search(SearchRequest request);
+}
+
+class Nested {
+ SearchRequest? request;
+}
+
+@HostApi(dartHostTestHandler: 'TestNestedApi')
+abstract class NestedApi {
+ SearchReply search(Nested nested);
+}
+
+void configurePigeon(PigeonOptions options) {
+ options.objcOptions ??= ObjcOptions();
+ options.objcOptions?.prefix = 'AC';
+ options.javaOptions ??= JavaOptions(className: 'Pigeon');
+ options.javaOptions?.package = 'dev.flutter.aaclarke.pigeon';
+}
+
+@FlutterApi()
+abstract class FlutterSearchApi {
+ SearchReply search(SearchRequest request);
+}
diff --git a/packages/pigeon/pigeons/void_arg_flutter.dart b/packages/pigeon/pigeons/void_arg_flutter.dart
new file mode 100644
index 0000000..d6fbd54
--- /dev/null
+++ b/packages/pigeon/pigeons/void_arg_flutter.dart
@@ -0,0 +1,14 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class Result {
+ int? code;
+}
+
+@FlutterApi()
+abstract class Api {
+ Result getCode();
+}
diff --git a/packages/pigeon/pigeons/void_arg_host.dart b/packages/pigeon/pigeons/void_arg_host.dart
new file mode 100644
index 0000000..fec04ab
--- /dev/null
+++ b/packages/pigeon/pigeons/void_arg_host.dart
@@ -0,0 +1,14 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class Result {
+ int? code;
+}
+
+@HostApi()
+abstract class Api {
+ Result getCode();
+}
diff --git a/packages/pigeon/pigeons/voidflutter.dart b/packages/pigeon/pigeons/voidflutter.dart
new file mode 100644
index 0000000..eb2faa9
--- /dev/null
+++ b/packages/pigeon/pigeons/voidflutter.dart
@@ -0,0 +1,14 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class SetRequest {
+ int? value;
+}
+
+@FlutterApi()
+abstract class Api {
+ void setValue(SetRequest request);
+}
diff --git a/packages/pigeon/pigeons/voidhost.dart b/packages/pigeon/pigeons/voidhost.dart
new file mode 100644
index 0000000..cdbbebd
--- /dev/null
+++ b/packages/pigeon/pigeons/voidhost.dart
@@ -0,0 +1,14 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class SetRequest {
+ int? value;
+}
+
+@HostApi()
+abstract class Api {
+ void setValue(SetRequest request);
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/.gitignore b/packages/pigeon/platform_tests/android_unit_tests/.gitignore
new file mode 100644
index 0000000..0fa6b67
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/.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/pigeon/platform_tests/android_unit_tests/.metadata b/packages/pigeon/platform_tests/android_unit_tests/.metadata
new file mode 100644
index 0000000..62b61c3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/.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: 891511d58f6550ce9e9b03b8d7c6a602caa97488
+ channel: master
+
+project_type: app
diff --git a/packages/pigeon/platform_tests/android_unit_tests/README.md b/packages/pigeon/platform_tests/android_unit_tests/README.md
new file mode 100644
index 0000000..e109aa5
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/README.md
@@ -0,0 +1,3 @@
+# android_unit_tests
+
+Unit-tests for Pigeon generated Java code. See [../../run_tests.sh](../../run_tests.sh).
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/.gitignore b/packages/pigeon/platform_tests/android_unit_tests/android/.gitignore
new file mode 100644
index 0000000..0a741cb
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/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/pigeon/platform_tests/android_unit_tests/android/app/build.gradle b/packages/pigeon/platform_tests/android_unit_tests/android/app/build.gradle
new file mode 100644
index 0000000..f45bc0b
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/build.gradle
@@ -0,0 +1,65 @@
+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 30
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId "com.example.android_unit_tests"
+ minSdkVersion 16
+ targetSdkVersion 30
+ 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
+ }
+ }
+
+ testOptions {
+ unitTests.returnDefaultValues = true
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ testImplementation 'junit:junit:4.+'
+ testImplementation "org.mockito:mockito-core:3.+"
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/debug/AndroidManifest.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..0102266
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android_unit_tests">
+ <!-- Flutter needs it to communicate with the running application
+ to allow setting breakpoints, to provide hot reload, etc.
+ -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/AndroidManifest.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a945186
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android_unit_tests">
+ <application
+ android:label="android_unit_tests"
+ android:icon="@mipmap/ic_launcher">
+ <activity
+ android:name=".MainActivity"
+ android:launchMode="singleTop"
+ android:theme="@style/LaunchTheme"
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+ android:hardwareAccelerated="true"
+ android:windowSoftInputMode="adjustResize">
+ <!-- Specifies an Android theme to apply to this Activity as soon as
+ the Android process has started. This theme is visible to the user
+ while the Flutter UI initializes. After that, this theme continues
+ to determine the Window background behind the Flutter UI. -->
+ <meta-data
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme"
+ />
+ <!-- Displays an Android View that continues showing the launch screen
+ Drawable until Flutter paints its first frame, then this splash
+ screen fades out. A splash screen is useful to avoid any visual
+ gap between the end of Android's launch screen and the painting of
+ Flutter's first frame. -->
+ <meta-data
+ android:name="io.flutter.embedding.android.SplashScreenDrawable"
+ android:resource="@drawable/launch_background"
+ />
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <!-- Don't delete the meta-data below.
+ This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+ <meta-data
+ android:name="flutterEmbedding"
+ android:value="2" />
+ </application>
+</manifest>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/java/com/example/android_unit_tests/Pigeon.java b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/java/com/example/android_unit_tests/Pigeon.java
new file mode 100644
index 0000000..6219796
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/java/com/example/android_unit_tests/Pigeon.java
@@ -0,0 +1,81 @@
+// Autogenerated from Pigeon (v0.2.0), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+package com.example.android_unit_tests;
+
+import io.flutter.plugin.common.BasicMessageChannel;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.StandardMessageCodec;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Generated class from Pigeon. */
+@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})
+public class Pigeon {
+
+ /** Generated class from Pigeon that represents data sent in messages. */
+ public static class SetRequest {
+ private Long value;
+
+ public Long getValue() {
+ return value;
+ }
+
+ public void setValue(Long setterArg) {
+ this.value = setterArg;
+ }
+
+ Map<String, Object> toMap() {
+ Map<String, Object> toMapResult = new HashMap<>();
+ toMapResult.put("value", value);
+ return toMapResult;
+ }
+
+ static SetRequest fromMap(Map<String, Object> map) {
+ SetRequest fromMapResult = new SetRequest();
+ Object value = map.get("value");
+ fromMapResult.value =
+ (value == null) ? null : ((value instanceof Integer) ? (Integer) value : (Long) value);
+ return fromMapResult;
+ }
+ }
+
+ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+ public interface Api {
+ void setValue(SetRequest arg);
+
+ /** Sets up an instance of `Api` to handle messages through the `binaryMessenger`. */
+ static void setup(BinaryMessenger binaryMessenger, Api api) {
+ {
+ BasicMessageChannel<Object> channel =
+ new BasicMessageChannel<>(
+ binaryMessenger, "dev.flutter.pigeon.Api.setValue", new StandardMessageCodec());
+ if (api != null) {
+ channel.setMessageHandler(
+ (message, reply) -> {
+ Map<String, Object> wrapped = new HashMap<>();
+ try {
+ @SuppressWarnings("ConstantConditions")
+ SetRequest input = SetRequest.fromMap((Map<String, Object>) message);
+ api.setValue(input);
+ wrapped.put("result", null);
+ } catch (Error | RuntimeException exception) {
+ wrapped.put("error", wrapError(exception));
+ }
+ reply.reply(wrapped);
+ });
+ } else {
+ channel.setMessageHandler(null);
+ }
+ }
+ }
+ }
+
+ private static Map<String, Object> wrapError(Throwable exception) {
+ Map<String, Object> errorMap = new HashMap<>();
+ errorMap.put("message", exception.toString());
+ errorMap.put("code", exception.getClass().getSimpleName());
+ errorMap.put("details", null);
+ return errorMap;
+ }
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/kotlin/com/example/android_unit_tests/MainActivity.kt b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/kotlin/com/example/android_unit_tests/MainActivity.kt
new file mode 100644
index 0000000..eb1afff
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/kotlin/com/example/android_unit_tests/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.example.android_unit_tests
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="?android:colorBackground" />
+
+ <!-- You can insert your own image assets here -->
+ <!-- <item>
+ <bitmap
+ android:gravity="center"
+ android:src="@mipmap/launch_image" />
+ </item> -->
+</layer-list>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable/launch_background.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@android:color/white" />
+
+ <!-- You can insert your own image assets here -->
+ <!-- <item>
+ <bitmap
+ android:gravity="center"
+ android:src="@mipmap/launch_image" />
+ </item> -->
+</layer-list>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values-night/styles.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..449a9f9
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
+ <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+ <!-- Show a splash screen on the activity. Automatically removed when
+ Flutter draws its first frame -->
+ <item name="android:windowBackground">@drawable/launch_background</item>
+ </style>
+ <!-- Theme applied to the Android Window as soon as the process has started.
+ This theme determines the color of the Android Window while your
+ Flutter UI initializes, as well as behind your Flutter UI while its
+ running.
+
+ This Theme is only used starting with V2 of Flutter's Android embedding. -->
+ <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+ <item name="android:windowBackground">?android:colorBackground</item>
+ </style>
+</resources>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values/styles.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..d74aa35
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
+ <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
+ <!-- Show a splash screen on the activity. Automatically removed when
+ Flutter draws its first frame -->
+ <item name="android:windowBackground">@drawable/launch_background</item>
+ </style>
+ <!-- Theme applied to the Android Window as soon as the process has started.
+ This theme determines the color of the Android Window while your
+ Flutter UI initializes, as well as behind your Flutter UI while its
+ running.
+
+ This Theme is only used starting with V2 of Flutter's Android embedding. -->
+ <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
+ <item name="android:windowBackground">?android:colorBackground</item>
+ </style>
+</resources>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/profile/AndroidManifest.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..0102266
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android_unit_tests">
+ <!-- Flutter needs it to communicate with the running application
+ to allow setting breakpoints, to provide hot reload, etc.
+ -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/PigeonTest.java b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/PigeonTest.java
new file mode 100644
index 0000000..1efa547
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/PigeonTest.java
@@ -0,0 +1,98 @@
+package com.example.android_unit_tests;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MessageCodec;
+import io.flutter.plugin.common.StandardMessageCodec;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+public class PigeonTest {
+ private MessageCodec<Object> codec = StandardMessageCodec.INSTANCE;
+
+ @Test
+ public void toMapAndBack() {
+ Pigeon.SetRequest request = new Pigeon.SetRequest();
+ request.setValue(1234l);
+ Map<String, Object> map = request.toMap();
+ Pigeon.SetRequest readRequest = Pigeon.SetRequest.fromMap(map);
+ assertEquals(request.getValue(), readRequest.getValue());
+ }
+
+ @Test
+ public void toMapAndBackNested() {
+ Pigeon.NestedRequest nested = new Pigeon.NestedRequest();
+ Pigeon.SetRequest request = new Pigeon.SetRequest();
+ request.setValue(1234l);
+ nested.setRequest(request);
+ Map<String, Object> map = nested.toMap();
+ Pigeon.NestedRequest readNested = Pigeon.NestedRequest.fromMap(map);
+ assertEquals(nested.getRequest().getValue(), readNested.getRequest().getValue());
+ }
+
+ @Test
+ public void clearsHandler() {
+ Pigeon.Api mockApi = mock(Pigeon.Api.class);
+ BinaryMessenger binaryMessenger = mock(BinaryMessenger.class);
+ Pigeon.Api.setup(binaryMessenger, mockApi);
+ ArgumentCaptor<String> channelName = ArgumentCaptor.forClass(String.class);
+ verify(binaryMessenger).setMessageHandler(channelName.capture(), isNotNull());
+ Pigeon.Api.setup(binaryMessenger, null);
+ verify(binaryMessenger).setMessageHandler(eq(channelName.getValue()), isNull());
+ }
+
+ /** Causes an exception in the handler by passing in null when a SetRequest is expected. */
+ @Test
+ public void errorMessage() {
+ Pigeon.Api mockApi = mock(Pigeon.Api.class);
+ BinaryMessenger binaryMessenger = mock(BinaryMessenger.class);
+ Pigeon.Api.setup(binaryMessenger, mockApi);
+ ArgumentCaptor<BinaryMessenger.BinaryMessageHandler> handler =
+ ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
+ verify(binaryMessenger).setMessageHandler(anyString(), handler.capture());
+ ByteBuffer message = codec.encodeMessage(null);
+ handler
+ .getValue()
+ .onMessage(
+ message,
+ (bytes) -> {
+ bytes.rewind();
+ @SuppressWarnings("unchecked")
+ Map<String, Object> wrapped = (Map<String, Object>) codec.decodeMessage(bytes);
+ assertTrue(wrapped.containsKey("error"));
+ });
+ }
+
+ @Test
+ public void callsVoidMethod() {
+ Pigeon.Api mockApi = mock(Pigeon.Api.class);
+ BinaryMessenger binaryMessenger = mock(BinaryMessenger.class);
+ Pigeon.Api.setup(binaryMessenger, mockApi);
+ ArgumentCaptor<BinaryMessenger.BinaryMessageHandler> handler =
+ ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
+ verify(binaryMessenger).setMessageHandler(anyString(), handler.capture());
+ Pigeon.SetRequest request = new Pigeon.SetRequest();
+ request.setValue(1234l);
+ ByteBuffer message = codec.encodeMessage(request.toMap());
+ message.rewind();
+ handler
+ .getValue()
+ .onMessage(
+ message,
+ (bytes) -> {
+ bytes.rewind();
+ @SuppressWarnings("unchecked")
+ Map<String, Object> wrapped = (Map<String, Object>) codec.decodeMessage(bytes);
+ assertTrue(wrapped.containsKey("result"));
+ assertNull(wrapped.get("result"));
+ });
+ ArgumentCaptor<Pigeon.SetRequest> receivedRequest =
+ ArgumentCaptor.forClass(Pigeon.SetRequest.class);
+ verify(mockApi).setValue(receivedRequest.capture());
+ assertEquals(request.getValue(), receivedRequest.getValue().getValue());
+ }
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/build.gradle b/packages/pigeon/platform_tests/android_unit_tests/android/build.gradle
new file mode 100644
index 0000000..8a8ae3d
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/build.gradle
@@ -0,0 +1,34 @@
+buildscript {
+ ext.kotlin_version = '1.3.50'
+ repositories {
+ google()
+ jcenter()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.1.0'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+ gradle.projectsEvaluated {
+ tasks.withType(JavaCompile) {
+ options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
+ }
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+ project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/gradle.properties b/packages/pigeon/platform_tests/android_unit_tests/android/gradle.properties
new file mode 100644
index 0000000..94adc3a
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/gradle/wrapper/gradle-wrapper.properties b/packages/pigeon/platform_tests/android_unit_tests/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..bc6a58a
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/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/pigeon/platform_tests/android_unit_tests/android/settings.gradle b/packages/pigeon/platform_tests/android_unit_tests/android/settings.gradle
new file mode 100644
index 0000000..44e62bc
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/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/pigeon/platform_tests/android_unit_tests/lib/main.dart b/packages/pigeon/platform_tests/android_unit_tests/lib/main.dart
new file mode 100644
index 0000000..5e49fc1
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/lib/main.dart
@@ -0,0 +1,5 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+void main() {}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/pubspec.yaml b/packages/pigeon/platform_tests/android_unit_tests/pubspec.yaml
new file mode 100644
index 0000000..febf7cf
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/pubspec.yaml
@@ -0,0 +1,20 @@
+name: android_unit_tests
+description: Unit tests for Pigeon generated Java code.
+
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+version: 1.0.0+1
+
+environment:
+ sdk: ">=2.7.0 <3.0.0"
+
+dependencies:
+ cupertino_icons: ^1.0.2
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+flutter:
+ uses-material-design: true
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/.gitignore b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/.gitignore
new file mode 100644
index 0000000..0fa6b67
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/.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/pigeon/platform_tests/flutter_null_safe_unit_tests/.metadata b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/.metadata
new file mode 100644
index 0000000..5db57c1
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/.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: 6db3d61ed4a58ba89140d7fe1fd294b598cc29c5
+ channel: master
+
+project_type: app
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/README.md b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/README.md
new file mode 100644
index 0000000..3c52fc9
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/README.md
@@ -0,0 +1,3 @@
+# flutter_unit_tests
+
+Unit test scaffold for null safe Flutter projects.
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart
new file mode 100644
index 0000000..5154847
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart
@@ -0,0 +1,133 @@
+// Autogenerated from Pigeon (v0.2.0), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types
+// @dart = 2.12
+import 'dart:async';
+import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List;
+
+import 'package:flutter/services.dart';
+
+class SearchReply {
+ String? result;
+ String? error;
+
+ Object encode() {
+ final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+ pigeonMap['result'] = result;
+ pigeonMap['error'] = error;
+ return pigeonMap;
+ }
+
+ static SearchReply decode(Object message) {
+ final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+ return SearchReply()
+ ..result = pigeonMap['result'] as String?
+ ..error = pigeonMap['error'] as String?;
+ }
+}
+
+class SearchRequest {
+ String? query;
+
+ Object encode() {
+ final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+ pigeonMap['query'] = query;
+ return pigeonMap;
+ }
+
+ static SearchRequest decode(Object message) {
+ final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+ return SearchRequest()..query = pigeonMap['query'] as String?;
+ }
+}
+
+class SearchReplies {
+ List<Object?>? replies;
+
+ Object encode() {
+ final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+ pigeonMap['replies'] = replies;
+ return pigeonMap;
+ }
+
+ static SearchReplies decode(Object message) {
+ final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+ return SearchReplies()..replies = pigeonMap['replies'] as List<Object?>?;
+ }
+}
+
+class SearchRequests {
+ List<Object?>? requests;
+
+ Object encode() {
+ final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+ pigeonMap['requests'] = requests;
+ return pigeonMap;
+ }
+
+ static SearchRequests decode(Object message) {
+ final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+ return SearchRequests()..requests = pigeonMap['requests'] as List<Object?>?;
+ }
+}
+
+class Api {
+ /// Constructor for [Api]. The [binaryMessenger] named argument is
+ /// available for dependency injection. If it is left null, the default
+ /// BinaryMessenger will be used which routes to the host platform.
+ Api({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger;
+
+ final BinaryMessenger? _binaryMessenger;
+
+ Future<SearchReply> search(SearchRequest arg) async {
+ final Object encoded = arg.encode();
+ final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.Api.search', const StandardMessageCodec(),
+ binaryMessenger: _binaryMessenger);
+ final Map<Object?, Object?>? replyMap =
+ await channel.send(encoded) as Map<Object?, Object?>?;
+ if (replyMap == null) {
+ throw PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel.',
+ details: null,
+ );
+ } else if (replyMap['error'] != null) {
+ final Map<Object?, Object?> error =
+ (replyMap['error'] as Map<Object?, Object?>?)!;
+ throw PlatformException(
+ code: (error['code'] as String?)!,
+ message: error['message'] as String?,
+ details: error['details'],
+ );
+ } else {
+ return SearchReply.decode(replyMap['result']!);
+ }
+ }
+
+ Future<SearchReplies> doSearches(SearchRequests arg) async {
+ final Object encoded = arg.encode();
+ final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.Api.doSearches', const StandardMessageCodec(),
+ binaryMessenger: _binaryMessenger);
+ final Map<Object?, Object?>? replyMap =
+ await channel.send(encoded) as Map<Object?, Object?>?;
+ if (replyMap == null) {
+ throw PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel.',
+ details: null,
+ );
+ } else if (replyMap['error'] != null) {
+ final Map<Object?, Object?> error =
+ (replyMap['error'] as Map<Object?, Object?>?)!;
+ throw PlatformException(
+ code: (error['code'] as String?)!,
+ message: error['message'] as String?,
+ details: error['details'],
+ );
+ } else {
+ return SearchReplies.decode(replyMap['result']!);
+ }
+ }
+}
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/pubspec.yaml b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/pubspec.yaml
new file mode 100644
index 0000000..9b21234
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/pubspec.yaml
@@ -0,0 +1,19 @@
+name: flutter_unit_tests
+description: Unit test scaffold for null safe Flutter projects.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+version: 1.0.0+1
+environment:
+ sdk: ">=2.12.0 <3.0.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ build_runner: ^1.11.0
+ flutter_test:
+ sdk: flutter
+ mockito: ^5.0.4
+
+flutter:
+ uses-material-design: true
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart
new file mode 100644
index 0000000..c407fd9
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart
@@ -0,0 +1,74 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter/services.dart';
+import 'package:flutter_unit_tests/null_safe_pigeon.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'null_safe_test.mocks.dart';
+
+@GenerateMocks(<Type>[BinaryMessenger])
+void main() {
+ test('with values filled', () {
+ final SearchReply reply = SearchReply()
+ ..result = 'foo'
+ ..error = 'bar';
+ final Object encoded = reply.encode();
+ final SearchReply decoded = SearchReply.decode(encoded);
+ expect(reply.result, decoded.result);
+ expect(reply.error, decoded.error);
+ });
+
+ test('with null value', () {
+ final SearchReply reply = SearchReply()
+ ..result = 'foo'
+ ..error = null;
+ final Object encoded = reply.encode();
+ final SearchReply decoded = SearchReply.decode(encoded);
+ expect(reply.result, decoded.result);
+ expect(reply.error, decoded.error);
+ });
+
+ test('send/receive', () async {
+ final SearchRequest request = SearchRequest()..query = 'hey';
+ final SearchReply reply = SearchReply()..result = 'ho';
+ final BinaryMessenger mockMessenger = MockBinaryMessenger();
+ const MessageCodec<Object?> codec = StandardMessageCodec();
+ final Completer<ByteData?> completer = Completer<ByteData?>();
+ completer.complete(
+ codec.encodeMessage(<String, Object>{'result': reply.encode()}));
+ final Future<ByteData?> sendResult = completer.future;
+ when(mockMessenger.send('dev.flutter.pigeon.Api.search', any))
+ .thenAnswer((Invocation realInvocation) => sendResult);
+ final Api api = Api(binaryMessenger: mockMessenger);
+ final SearchReply readReply = await api.search(request);
+ expect(readReply, isNotNull);
+ expect(reply.result, readReply.result);
+ });
+
+ // TODO(gaaclarke): This test is a companion for the fix to https://github.com/flutter/flutter/issues/80538
+ // test('send/receive list classes', () async {
+ // final SearchRequest request = SearchRequest()
+ // ..query = 'hey';
+ // final SearchReply reply = SearchReply()
+ // ..result = 'ho';
+ // final SearchRequests requests = SearchRequests()
+ // ..requests = <SearchRequest>[request];
+ // final SearchReplies replies = SearchReplies()
+ // ..replies = <SearchReply>[reply];
+ // final BinaryMessenger mockMessenger = MockBinaryMessenger();
+ // const MessageCodec<Object?> codec = StandardMessageCodec();
+ // final Completer<ByteData?> completer = Completer<ByteData?>();
+ // completer.complete(codec.encodeMessage(<String, Object>{'result' : replies.encode()}));
+ // final Future<ByteData?> sendResult = completer.future;
+ // when(mockMessenger.send('dev.flutter.pigeon.Api.search', any)).thenAnswer((Invocation realInvocation) => sendResult);
+ // final Api api = Api(binaryMessenger: mockMessenger);
+ // final SearchReplies readReplies = await api.doSearches(requests);
+ // expect(readReplies, isNotNull);
+ // expect(reply.result, (readReplies.replies![0] as SearchReply?)!.result);
+ // });
+}
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.mocks.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.mocks.dart
new file mode 100644
index 0000000..5a326b5
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.mocks.dart
@@ -0,0 +1,55 @@
+// Mocks generated by Mockito 5.0.4 from annotations
+// in flutter_unit_tests/test/null_safe_test.dart.
+// Do not manually edit this file.
+
+import 'dart:async' as _i3;
+import 'dart:typed_data' as _i4;
+import 'dart:ui' as _i5;
+
+import 'package:flutter/src/services/binary_messenger.dart' as _i2;
+import 'package:mockito/mockito.dart' as _i1;
+
+// ignore_for_file: comment_references
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: always_specify_types
+
+/// A class which mocks [BinaryMessenger].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockBinaryMessenger extends _i1.Mock implements _i2.BinaryMessenger {
+ MockBinaryMessenger() {
+ _i1.throwOnMissingStub(this);
+ }
+
+ @override
+ _i3.Future<void> handlePlatformMessage(String? channel, _i4.ByteData? data,
+ _i5.PlatformMessageResponseCallback? callback) =>
+ (super.noSuchMethod(
+ Invocation.method(#handlePlatformMessage, [channel, data, callback]),
+ returnValue: Future<void>.value(null),
+ returnValueForMissingStub:
+ Future<dynamic>.value()) as _i3.Future<void>);
+ @override
+ _i3.Future<_i4.ByteData?>? send(String? channel, _i4.ByteData? message) =>
+ (super.noSuchMethod(Invocation.method(#send, [channel, message]))
+ as _i3.Future<_i4.ByteData?>?);
+ @override
+ void setMessageHandler(String? channel, _i2.MessageHandler? handler) => super
+ .noSuchMethod(Invocation.method(#setMessageHandler, [channel, handler]),
+ returnValueForMissingStub: null);
+ @override
+ bool checkMessageHandler(String? channel, _i2.MessageHandler? handler) =>
+ (super.noSuchMethod(
+ Invocation.method(#checkMessageHandler, [channel, handler]),
+ returnValue: false) as bool);
+ @override
+ void setMockMessageHandler(String? channel, _i2.MessageHandler? handler) =>
+ super.noSuchMethod(
+ Invocation.method(#setMockMessageHandler, [channel, handler]),
+ returnValueForMissingStub: null);
+ @override
+ bool checkMockMessageHandler(String? channel, _i2.MessageHandler? handler) =>
+ (super.noSuchMethod(
+ Invocation.method(#checkMockMessageHandler, [channel, handler]),
+ returnValue: false) as bool);
+}
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/.gitignore b/packages/pigeon/platform_tests/ios_unit_tests/.gitignore
new file mode 100644
index 0000000..ae1f183
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/.gitignore
@@ -0,0 +1,37 @@
+# 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
+
+# Exceptions to above rules.
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/.metadata b/packages/pigeon/platform_tests/ios_unit_tests/.metadata
new file mode 100644
index 0000000..01d2dcb
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/.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: 0b8abb4724aa590dd0f429683339b1e045a1594d
+ channel: stable
+
+project_type: app
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/README.md b/packages/pigeon/platform_tests/ios_unit_tests/README.md
new file mode 100644
index 0000000..3b17696
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/README.md
@@ -0,0 +1,16 @@
+# ios_unit_tests
+
+A new Flutter project.
+
+## 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/pigeon/platform_tests/ios_unit_tests/ios/.gitignore b/packages/pigeon/platform_tests/ios_unit_tests/ios/.gitignore
new file mode 100644
index 0000000..e96ef60
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/.gitignore
@@ -0,0 +1,32 @@
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/AppFrameworkInfo.plist b/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..6b4c0f7
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>App</string>
+ <key>CFBundleIdentifier</key>
+ <string>io.flutter.flutter.app</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>App</string>
+ <key>CFBundlePackageType</key>
+ <string>FMWK</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>MinimumOSVersion</key>
+ <string>8.0</string>
+</dict>
+</plist>
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/Debug.xcconfig b/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/Debug.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/Release.xcconfig b/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/Release.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..cca797a
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,667 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 0D50126D23FF759100CD5B95 /* messages.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D50126B23FF759100CD5B95 /* messages.m */; };
+ 0D50127523FF75B100CD5B95 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D50127423FF75B100CD5B95 /* RunnerTests.m */; };
+ 0D8C35E825D45A3000B76435 /* async_handlers.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D8C35E725D45A3000B76435 /* async_handlers.m */; };
+ 0D8C35EB25D45A7900B76435 /* AsyncHandlersTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D8C35EA25D45A7900B76435 /* AsyncHandlersTest.m */; };
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
+ 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 0D50127723FF75B100CD5B95 /* 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 */
+ 0D50126B23FF759100CD5B95 /* messages.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = messages.m; sourceTree = "<group>"; };
+ 0D50126C23FF759100CD5B95 /* messages.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = messages.h; sourceTree = "<group>"; };
+ 0D50127223FF75B100CD5B95 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 0D50127423FF75B100CD5B95 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = "<group>"; };
+ 0D50127623FF75B100CD5B95 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ 0D8C35E625D45A3000B76435 /* async_handlers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = async_handlers.h; sourceTree = "<group>"; };
+ 0D8C35E725D45A3000B76435 /* async_handlers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = async_handlers.m; sourceTree = "<group>"; };
+ 0D8C35EA25D45A7900B76435 /* AsyncHandlersTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AsyncHandlersTest.m; sourceTree = "<group>"; };
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
+ 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
+ 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
+ 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 = "<group>"; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 0D50126F23FF75B100CD5B95 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 0D50127323FF75B100CD5B95 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 0D50127423FF75B100CD5B95 /* RunnerTests.m */,
+ 0D50127623FF75B100CD5B95 /* Info.plist */,
+ 0D8C35EA25D45A7900B76435 /* AsyncHandlersTest.m */,
+ );
+ path = RunnerTests;
+ sourceTree = "<group>";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "<group>";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 0D50127323FF75B100CD5B95 /* RunnerTests */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ );
+ sourceTree = "<group>";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ 0D50127223FF75B100CD5B95 /* RunnerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 0D8C35E625D45A3000B76435 /* async_handlers.h */,
+ 0D8C35E725D45A3000B76435 /* async_handlers.m */,
+ 0D50126C23FF759100CD5B95 /* messages.h */,
+ 0D50126B23FF759100CD5B95 /* messages.m */,
+ 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 = "<group>";
+ };
+ 97C146F11CF9000F007C117D /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146F21CF9000F007C117D /* main.m */,
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 0D50127123FF75B100CD5B95 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 0D50127923FF75B100CD5B95 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 0D50126E23FF75B100CD5B95 /* Sources */,
+ 0D50126F23FF75B100CD5B95 /* Frameworks */,
+ 0D50127023FF75B100CD5B95 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 0D50127823FF75B100CD5B95 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 0D50127223FF75B100CD5B95 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1020;
+ ORGANIZATIONNAME = "The Chromium Authors";
+ TargetAttributes = {
+ 0D50127123FF75B100CD5B95 = {
+ CreatedOnToolsVersion = 11.3;
+ 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 */,
+ 0D50127123FF75B100CD5B95 /* RunnerTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 0D50127023FF75B100CD5B95 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 0D50126E23FF75B100CD5B95 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0D50127523FF75B100CD5B95 /* RunnerTests.m in Sources */,
+ 0D8C35EB25D45A7900B76435 /* AsyncHandlersTest.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
+ 0D50126D23FF759100CD5B95 /* messages.m in Sources */,
+ 97C146F31CF9000F007C117D /* main.m in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ 0D8C35E825D45A3000B76435 /* async_handlers.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 0D50127823FF75B100CD5B95 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 97C146ED1CF9000F007C117D /* Runner */;
+ targetProxy = 0D50127723FF75B100CD5B95 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "<group>";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 0D50127A23FF75B100CD5B95 /* 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.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.aaclarke.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+ };
+ name = Debug;
+ };
+ 0D50127B23FF75B100CD5B95 /* 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.2;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.aaclarke.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+ };
+ name = Release;
+ };
+ 0D50127C23FF75B100CD5B95 /* 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.2;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.aaclarke.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+ };
+ name = Profile;
+ };
+ 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 = 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.iosUnitTests;
+ 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 = 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_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.iosUnitTests;
+ 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.iosUnitTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 0D50127923FF75B100CD5B95 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 0D50127A23FF75B100CD5B95 /* Debug */,
+ 0D50127B23FF75B100CD5B95 /* Release */,
+ 0D50127C23FF75B100CD5B95 /* 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/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "group:Runner.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..97e0ecb
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "1020"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "0D50127123FF75B100CD5B95"
+ BuildableName = "RunnerTests.xctest"
+ BlueprintName = "RunnerTests"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Profile"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerTests.xcscheme b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerTests.xcscheme
new file mode 100644
index 0000000..606dad1
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerTests.xcscheme
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "1130"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "0D50127123FF75B100CD5B95"
+ BuildableName = "RunnerTests.xctest"
+ BlueprintName = "RunnerTests"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "group:Runner.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IDEDidComputeMac32BitWarning</key>
+ <true/>
+</dict>
+</plist>
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/AppDelegate.h b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/AppDelegate.h
new file mode 100644
index 0000000..36e21bb
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/AppDelegate.h
@@ -0,0 +1,6 @@
+#import <Flutter/Flutter.h>
+#import <UIKit/UIKit.h>
+
+@interface AppDelegate : FlutterAppDelegate
+
+@end
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/AppDelegate.m b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/AppDelegate.m
new file mode 100644
index 0000000..70e8393
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/AppDelegate.m
@@ -0,0 +1,13 @@
+#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/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..ec8c2c5
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,104 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "size" : "20x20",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "20x20",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "60x60",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "20x20",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "20x20",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "29x29",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "40x40",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "76x76",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "76x76",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "83.5x83.5",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ios-marketing",
+ "size" : "1024x1024",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..3efa7d1
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
+ <viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
+ </imageView>
+ </subviews>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ <constraints>
+ <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
+ <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
+ </constraints>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+ <resources>
+ <image name="LaunchImage" width="168" height="185"/>
+ </resources>
+</document>
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Base.lproj/Main.storyboard b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ </dependencies>
+ <scenes>
+ <!--Flutter View Controller-->
+ <scene sceneID="tne-QT-ifu">
+ <objects>
+ <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+ <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+ </objects>
+ </scene>
+ </scenes>
+</document>
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Info.plist b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Info.plist
new file mode 100644
index 0000000..444570d
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>ios_unit_tests</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>$(FLUTTER_BUILD_NAME)</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>$(FLUTTER_BUILD_NUMBER)</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UIViewControllerBasedStatusBarAppearance</key>
+ <false/>
+</dict>
+</plist>
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.h b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.h
new file mode 100644
index 0000000..a69bdb6
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.h
@@ -0,0 +1,27 @@
+// Autogenerated from Pigeon (v0.1.20), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+#import <Foundation/Foundation.h>
+@protocol FlutterBinaryMessenger;
+@class FlutterError;
+@class FlutterStandardTypedData;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class Value;
+
+@interface Value : NSObject
+@property(nonatomic, strong, nullable) NSNumber *number;
+@end
+
+@interface Api2Flutter : NSObject
+- (instancetype)initWithBinaryMessenger:(id<FlutterBinaryMessenger>)binaryMessenger;
+- (void)calculate:(Value *)input completion:(void (^)(Value *, NSError *_Nullable))completion;
+@end
+@protocol Api2Host
+- (void)calculate:(nullable Value *)input
+ completion:(void (^)(Value *_Nullable, FlutterError *_Nullable))completion;
+@end
+
+extern void Api2HostSetup(id<FlutterBinaryMessenger> binaryMessenger, id<Api2Host> _Nullable api);
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.m b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.m
new file mode 100644
index 0000000..73f062e
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.m
@@ -0,0 +1,88 @@
+// Autogenerated from Pigeon (v0.1.20), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+#import "async_handlers.h"
+#import <Flutter/Flutter.h>
+
+#if !__has_feature(objc_arc)
+#error File requires ARC to be enabled.
+#endif
+
+static NSDictionary<NSString *, id> *wrapResult(NSDictionary *result, FlutterError *error) {
+ NSDictionary *errorDict = (NSDictionary *)[NSNull null];
+ if (error) {
+ errorDict = @{
+ @"code" : (error.code ? error.code : [NSNull null]),
+ @"message" : (error.message ? error.message : [NSNull null]),
+ @"details" : (error.details ? error.details : [NSNull null]),
+ };
+ }
+ return @{
+ @"result" : (result ? result : [NSNull null]),
+ @"error" : errorDict,
+ };
+}
+
+@interface Value ()
++ (Value *)fromMap:(NSDictionary *)dict;
+- (NSDictionary *)toMap;
+@end
+
+@implementation Value
++ (Value *)fromMap:(NSDictionary *)dict {
+ Value *result = [[Value alloc] init];
+ result.number = dict[@"number"];
+ if ((NSNull *)result.number == [NSNull null]) {
+ result.number = nil;
+ }
+ return result;
+}
+- (NSDictionary *)toMap {
+ return [NSDictionary
+ dictionaryWithObjectsAndKeys:(self.number ? self.number : [NSNull null]), @"number", nil];
+}
+@end
+
+@interface Api2Flutter ()
+@property(nonatomic, strong) NSObject<FlutterBinaryMessenger> *binaryMessenger;
+@end
+
+@implementation Api2Flutter
+- (instancetype)initWithBinaryMessenger:(NSObject<FlutterBinaryMessenger> *)binaryMessenger {
+ self = [super init];
+ if (self) {
+ _binaryMessenger = binaryMessenger;
+ }
+ return self;
+}
+
+- (void)calculate:(Value *)input completion:(void (^)(Value *, NSError *_Nullable))completion {
+ FlutterBasicMessageChannel *channel =
+ [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.Api2Flutter.calculate"
+ binaryMessenger:self.binaryMessenger];
+ NSDictionary *inputMap = [input toMap];
+ [channel sendMessage:inputMap
+ reply:^(id reply) {
+ NSDictionary *outputMap = reply;
+ Value *output = [Value fromMap:outputMap];
+ completion(output, nil);
+ }];
+}
+@end
+void Api2HostSetup(id<FlutterBinaryMessenger> binaryMessenger, id<Api2Host> api) {
+ {
+ FlutterBasicMessageChannel *channel =
+ [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.Api2Host.calculate"
+ binaryMessenger:binaryMessenger];
+ if (api) {
+ [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
+ Value *input = [Value fromMap:message];
+ [api calculate:input
+ completion:^(Value *_Nullable output, FlutterError *_Nullable error) {
+ callback(wrapResult([output toMap], error));
+ }];
+ }];
+ } else {
+ [channel setMessageHandler:nil];
+ }
+ }
+}
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/main.m b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/main.m
new file mode 100644
index 0000000..dff6597
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/main.m
@@ -0,0 +1,9 @@
+#import <Flutter/Flutter.h>
+#import <UIKit/UIKit.h>
+#import "AppDelegate.h"
+
+int main(int argc, char* argv[]) {
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
+ }
+}
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/AsyncHandlersTest.m b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/AsyncHandlersTest.m
new file mode 100644
index 0000000..f32e046
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/AsyncHandlersTest.m
@@ -0,0 +1,139 @@
+#import <Flutter/Flutter.h>
+#import <XCTest/XCTest.h>
+#import "async_handlers.h"
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@interface Value ()
++ (Value*)fromMap:(NSDictionary*)dict;
+- (NSDictionary*)toMap;
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@interface MockBinaryMessenger : NSObject<FlutterBinaryMessenger>
+@property(nonatomic, copy) NSNumber* result;
+@property(nonatomic, retain) FlutterStandardMessageCodec* codec;
+@property(nonatomic, retain) NSMutableDictionary<NSString*, FlutterBinaryMessageHandler>* handlers;
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@implementation MockBinaryMessenger
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _codec = [FlutterStandardMessageCodec sharedInstance];
+ _handlers = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+- (void)cleanupConnection:(FlutterBinaryMessengerConnection)connection {
+}
+
+- (void)sendOnChannel:(nonnull NSString*)channel message:(NSData* _Nullable)message {
+}
+
+- (void)sendOnChannel:(nonnull NSString*)channel
+ message:(NSData* _Nullable)message
+ binaryReply:(FlutterBinaryReply _Nullable)callback {
+ if (self.result) {
+ Value* output = [[Value alloc] init];
+ output.number = self.result;
+ NSDictionary* outputDictionary = [output toMap];
+ callback([_codec encode:outputDictionary]);
+ }
+}
+
+- (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(nonnull NSString*)channel
+ binaryMessageHandler:
+ (FlutterBinaryMessageHandler _Nullable)handler {
+ _handlers[channel] = [handler copy];
+ return _handlers.count;
+}
+
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@interface MockApi2Host : NSObject<Api2Host>
+@property(nonatomic, copy) NSNumber* output;
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@implementation MockApi2Host
+
+- (void)calculate:(Value* _Nullable)input
+ completion:(nonnull void (^)(Value* _Nullable, FlutterError* _Nullable))completion {
+ if (self.output) {
+ Value* output = [[Value alloc] init];
+ output.number = self.output;
+ completion(output, nil);
+ } else {
+ completion(nil, [FlutterError errorWithCode:@"hey" message:@"ho" details:nil]);
+ }
+}
+
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@interface AsyncHandlersTest : XCTestCase
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@implementation AsyncHandlersTest
+
+- (void)testAsyncHost2Flutter {
+ MockBinaryMessenger* binaryMessenger = [[MockBinaryMessenger alloc] init];
+ binaryMessenger.result = @(2);
+ Api2Flutter* api2Flutter = [[Api2Flutter alloc] initWithBinaryMessenger:binaryMessenger];
+ Value* input = [[Value alloc] init];
+ input.number = @(1);
+ XCTestExpectation* expectation = [self expectationWithDescription:@"calculate callback"];
+ [api2Flutter calculate:input
+ completion:^(Value* _Nonnull output, NSError* _Nullable error) {
+ XCTAssertEqual(output.number.intValue, 2);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:1.0 handler:nil];
+}
+
+- (void)testAsyncFlutter2Host {
+ MockBinaryMessenger* binaryMessenger = [[MockBinaryMessenger alloc] init];
+ MockApi2Host* mockApi2Host = [[MockApi2Host alloc] init];
+ mockApi2Host.output = @(2);
+ Api2HostSetup(binaryMessenger, mockApi2Host);
+ NSString* channelName = @"dev.flutter.pigeon.Api2Host.calculate";
+ XCTAssertNotNil(binaryMessenger.handlers[channelName]);
+
+ Value* input = [[Value alloc] init];
+ input.number = @(1);
+ NSData* inputEncoded = [binaryMessenger.codec encode:[input toMap]];
+ XCTestExpectation* expectation = [self expectationWithDescription:@"calculate callback"];
+ binaryMessenger.handlers[channelName](inputEncoded, ^(NSData* data) {
+ NSDictionary* outputMap = [binaryMessenger.codec decode:data];
+ Value* output = [Value fromMap:outputMap[@"result"]];
+ XCTAssertEqual(output.number.intValue, 2);
+ [expectation fulfill];
+ });
+ [self waitForExpectationsWithTimeout:1.0 handler:nil];
+}
+
+- (void)testAsyncFlutter2HostError {
+ MockBinaryMessenger* binaryMessenger = [[MockBinaryMessenger alloc] init];
+ MockApi2Host* mockApi2Host = [[MockApi2Host alloc] init];
+ Api2HostSetup(binaryMessenger, mockApi2Host);
+ NSString* channelName = @"dev.flutter.pigeon.Api2Host.calculate";
+ XCTAssertNotNil(binaryMessenger.handlers[channelName]);
+
+ Value* input = [[Value alloc] init];
+ input.number = @(1);
+ NSData* inputEncoded = [binaryMessenger.codec encode:[input toMap]];
+ XCTestExpectation* expectation = [self expectationWithDescription:@"calculate callback"];
+ binaryMessenger.handlers[channelName](inputEncoded, ^(NSData* data) {
+ NSDictionary* outputMap = [binaryMessenger.codec decode:data];
+ XCTAssertNotNil(outputMap[@"error"]);
+ [expectation fulfill];
+ });
+ [self waitForExpectationsWithTimeout:1.0 handler:nil];
+}
+
+@end
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/Info.plist b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/Info.plist
new file mode 100644
index 0000000..64d65ca
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/RunnerTests.m b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/RunnerTests.m
new file mode 100644
index 0000000..f077fd4
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/RunnerTests.m
@@ -0,0 +1,39 @@
+#import <XCTest/XCTest.h>
+#import "messages.h"
+
+@interface ACSearchReply ()
++ (ACSearchReply*)fromMap:(NSDictionary*)dict;
+- (NSDictionary*)toMap;
+@end
+
+@interface RunnerTests : XCTestCase
+
+@end
+
+@implementation RunnerTests
+
+- (void)testToMapAndBack {
+ ACSearchReply* reply = [[ACSearchReply alloc] init];
+ reply.result = @"foobar";
+ NSDictionary* dict = [reply toMap];
+ ACSearchReply* copy = [ACSearchReply fromMap:dict];
+ XCTAssertEqual(reply.result, copy.result);
+}
+
+- (void)testHandlesNull {
+ ACSearchReply* reply = [[ACSearchReply alloc] init];
+ reply.result = nil;
+ NSDictionary* dict = [reply toMap];
+ ACSearchReply* copy = [ACSearchReply fromMap:dict];
+ XCTAssertNil(copy.result);
+}
+
+- (void)testHandlesNullFirst {
+ ACSearchReply* reply = [[ACSearchReply alloc] init];
+ reply.error = @"foobar";
+ NSDictionary* dict = [reply toMap];
+ ACSearchReply* copy = [ACSearchReply fromMap:dict];
+ XCTAssertEqual(reply.error, copy.error);
+}
+
+@end
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/lib/main.dart b/packages/pigeon/platform_tests/ios_unit_tests/lib/main.dart
new file mode 100644
index 0000000..e89b0e7
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/lib/main.dart
@@ -0,0 +1,14 @@
+import 'package:flutter/material.dart';
+
+void main() => runApp(const MyApp());
+
+/// An empty app.
+class MyApp extends StatelessWidget {
+ /// Creates an empty app.
+ const MyApp({Key key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/pubspec.yaml b/packages/pigeon/platform_tests/ios_unit_tests/pubspec.yaml
new file mode 100644
index 0000000..e5e7de4
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/pubspec.yaml
@@ -0,0 +1,69 @@
+name: ios_unit_tests
+description: A new Flutter project.
+
+# The following defines the version and build number for your application.
+# A version number is three numbers separated by dots, like 1.2.43
+# followed by an optional build number separated by a +.
+# Both the version and the builder number may be overridden in flutter
+# build by specifying --build-name and --build-number, respectively.
+# In Android, build-name is used as versionName while build-number used as versionCode.
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
+# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+version: 1.0.0+1
+
+environment:
+ sdk: ">=2.1.0 <3.0.0"
+
+dependencies:
+ cupertino_icons: ^0.1.2
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+
+# 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/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
new file mode 100644
index 0000000..06278fc
--- /dev/null
+++ b/packages/pigeon/pubspec.yaml
@@ -0,0 +1,11 @@
+name: pigeon
+version: 0.2.1 # This must match the version in lib/generator_tools.dart
+description: Code generator tool to make communication between Flutter and the host platform type-safe and easier.
+homepage: https://github.com/flutter/packages/tree/master/packages/pigeon
+dependencies:
+ args: ^2.0.0
+ path: ^1.8.0
+dev_dependencies:
+ test: ^1.11.1
+environment:
+ sdk: '>=2.12.0 <3.0.0'
diff --git a/packages/pigeon/run_tests.sh b/packages/pigeon/run_tests.sh
new file mode 100755
index 0000000..07d38bb
--- /dev/null
+++ b/packages/pigeon/run_tests.sh
@@ -0,0 +1,454 @@
+#!/bin/bash
+###############################################################################
+# run_tests.sh
+#
+# This runs all the different types of tests for pigeon. It should be run from
+# the directory that contains the script.
+###############################################################################
+
+# exit when any command fails
+set -e
+
+# TODO(blasten): Enable on stable when possible.
+# https://github.com/flutter/flutter/issues/75187
+if [[ "$CHANNEL" == "stable" ]]; then
+ exit 0
+fi
+
+###############################################################################
+# Variables
+###############################################################################
+flutter=$(which flutter)
+flutter_bin=$(dirname $flutter)
+framework_path="$flutter_bin/cache/artifacts/engine/ios/"
+
+java_linter=checkstyle-8.41-all.jar
+java_formatter=google-java-format-1.3-all-deps.jar
+google_checks=google_checks.xml
+google_checks_version=7190c47ca5515ad8cb827bc4065ae7664d2766c1
+java_error_prone=error_prone_core-2.5.1-with-dependencies.jar
+dataflow_shaded=dataflow-shaded-3.7.1.jar
+jformat_string=jFormatString-3.0.0.jar
+java_version=$(java -version 2>&1 | head -1 | cut -d'"' -f2 | sed '/^1\./s///' | cut -d'.' -f1)
+javac_jar=javac-9+181-r4173-1.jar
+if [ $java_version == "8" ]; then
+ javac_bootclasspath="-J-Xbootclasspath/p:ci/$javac_jar"
+else
+ javac_bootclasspath=
+fi
+
+###############################################################################
+# Helper Functions
+###############################################################################
+
+# Create a temporary directory in a way that works on both Linux and macOS.
+#
+# The mktemp commands have slighly semantics on the BSD systems vs GNU systems.
+mktmpdir() {
+ mktemp -d flutter_pigeon.XXXXXX 2>/dev/null || mktemp -d -t flutter_pigeon.
+}
+
+# test_pigeon_ios(<path to pigeon file>)
+#
+# Compiles the pigeon file to a temp directory and attempts to compile the objc
+# code.
+test_pigeon_ios() {
+ echo "test_pigeon_ios($1)"
+ temp_dir=$(mktmpdir)
+
+ pub run pigeon \
+ --no-dart_null_safety \
+ --input $1 \
+ --dart_out $temp_dir/pigeon.dart \
+ --objc_header_out $temp_dir/pigeon.h \
+ --objc_source_out $temp_dir/pigeon.m
+
+ xcrun clang \
+ -arch arm64 \
+ -isysroot $(xcrun --sdk iphoneos --show-sdk-path) \
+ -F $framework_path \
+ -F $framework_path/Flutter.xcframework/ios-armv7_arm64 \
+ -Werror \
+ -fobjc-arc \
+ -c $temp_dir/pigeon.m \
+ -o $temp_dir/pigeon.o
+
+ rm -rf $temp_dir
+}
+
+# test_pigeon_android(<path to pigeon file>)
+#
+# Compiles the pigeon file to a temp directory and attempts to compile the java
+# code.
+test_pigeon_android() {
+ echo "test_pigeon_android($1)"
+ temp_dir=$(mktmpdir)
+
+ pub run pigeon \
+ --input $1 \
+ --dart_out $temp_dir/pigeon.dart \
+ --java_out $temp_dir/Pigeon.java \
+ --java_package foo
+
+ java -jar ci/$java_formatter --replace "$temp_dir/Pigeon.java"
+ java -jar ci/$java_linter -c "ci/$google_checks" "$temp_dir/Pigeon.java"
+ if ! javac \
+ $javac_bootclasspath \
+ -XDcompilePolicy=simple \
+ -processorpath "ci/$java_error_prone:ci/$dataflow_shaded:ci/$jformat_string" \
+ '-Xplugin:ErrorProne -Xep:CatchingUnchecked:ERROR' \
+ -classpath "$flutter_bin/cache/artifacts/engine/android-x64/flutter.jar" \
+ $temp_dir/Pigeon.java; then
+ echo "javac $temp_dir/Pigeon.java failed"
+ exit 1
+ fi
+
+ rm -rf $temp_dir
+}
+
+# test_null_safe_dart(<path to pigeon file>)
+#
+# Compiles the pigeon file to a temp directory and attempts to run the dart
+# analyzer on it with and without null safety turned on.
+test_pigeon_dart() {
+ echo "test_pigeon_dart($1)"
+ temp_dir_1=$(mktmpdir)
+ temp_dir_2=$(mktmpdir)
+
+ pub run pigeon \
+ --input $1 \
+ --dart_out $temp_dir_1/pigeon.dart &
+ null_safe_gen_pid=$!
+
+ pub run pigeon \
+ --no-dart_null_safety \
+ --input $1 \
+ --dart_out $temp_dir_2/pigeon.dart &
+ non_null_safe_gen_pid=$!
+
+ wait $null_safe_gen_pid
+ wait $non_null_safe_gen_pid
+
+ # `./e2e_tests/test_objc/.packages` is used to get access to Flutter since
+ # Pigeon doesn't depend on Flutter.
+ dartanalyzer $temp_dir_1/pigeon.dart --fatal-infos --fatal-warnings --packages ./e2e_tests/test_objc/.packages &
+ null_safe_analyze_pid=$!
+ dartanalyzer $temp_dir_2/pigeon.dart --fatal-infos --fatal-warnings --packages ./e2e_tests/test_objc/.packages &
+ non_null_safe_analyze_pid=$!
+
+ wait $null_safe_analyze_pid
+ wait $non_null_safe_analyze_pid
+
+ rm -rf $temp_dir_1
+ rm -rf $temp_dir_2
+}
+
+print_usage() {
+ echo "usage: ./run_tests.sh [-l] [-t test_name]
+
+flags:
+ -t test_name: Run only specified test.
+ -l : List available tests.
+"
+}
+
+###############################################################################
+# Stages
+###############################################################################
+get_java_linter_formatter() {
+ if [ ! -f "ci/$java_linter" ]; then
+ curl -L https://github.com/checkstyle/checkstyle/releases/download/checkstyle-8.41/$java_linter >"ci/$java_linter"
+ fi
+ if [ ! -f "ci/$java_formatter" ]; then
+ curl -L https://github.com/google/google-java-format/releases/download/google-java-format-1.3/$java_formatter >"ci/$java_formatter"
+ fi
+ if [ ! -f "ci/$google_checks" ]; then
+ curl -L https://raw.githubusercontent.com/checkstyle/checkstyle/$google_checks_version/src/main/resources/$google_checks >"ci/$google_checks"
+ fi
+ if [ ! -f "ci/$java_error_prone" ]; then
+ curl https://repo1.maven.org/maven2/com/google/errorprone/error_prone_core/2.5.1/$java_error_prone >"ci/$java_error_prone"
+ fi
+ if [ ! -f "ci/$dataflow_shaded" ]; then
+ curl https://repo1.maven.org/maven2/org/checkerframework/dataflow-shaded/3.7.1/$dataflow_shaded >"ci/$dataflow_shaded"
+ fi
+ if [ ! -f "ci/$jformat_string" ]; then
+ curl https://repo1.maven.org/maven2/com/google/code/findbugs/jFormatString/3.0.0/$jformat_string >"ci/$jformat_string"
+ fi
+ if [ ! -f "ci/$javac_jar" ]; then
+ curl https://repo1.maven.org/maven2/com/google/errorprone/javac/9+181-r4173-1/$javac_jar >"ci/$javac_jar"
+ fi
+}
+
+run_dart_unittests() {
+ dart analyze bin
+ dart analyze lib
+ dart test
+}
+
+test_running_without_arguments() {
+ pub run pigeon 1>/dev/null
+}
+
+run_flutter_unittests() {
+ pushd $PWD
+ pub run pigeon \
+ --input pigeons/flutter_unittests.dart \
+ --dart_out platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart
+ cd platform_tests/flutter_null_safe_unit_tests
+ flutter pub get
+ flutter test test/null_safe_test.dart
+ popd
+}
+
+run_mock_handler_tests() {
+ pushd $PWD
+ pub run pigeon \
+ --input pigeons/message.dart \
+ --dart_out mock_handler_tester/test/message.dart \
+ --dart_test_out mock_handler_tester/test/test.dart
+ dartfmt -w mock_handler_tester/test/message.dart
+ dartfmt -w mock_handler_tester/test/test.dart
+ cd mock_handler_tester
+ flutter test
+ popd
+}
+
+run_dart_compilation_tests() {
+ # Make sure the artifacts are present.
+ flutter precache
+ # Make sure flutter dependencies are available.
+ pushd $PWD
+ cd e2e_tests/test_objc/
+ flutter pub get
+ popd
+ test_pigeon_dart ./pigeons/all_datatypes.dart
+ test_pigeon_dart ./pigeons/async_handlers.dart
+ test_pigeon_dart ./pigeons/host2flutter.dart
+ test_pigeon_dart ./pigeons/list.dart
+ test_pigeon_dart ./pigeons/message.dart
+ test_pigeon_dart ./pigeons/void_arg_flutter.dart
+ test_pigeon_dart ./pigeons/void_arg_host.dart
+ test_pigeon_dart ./pigeons/voidflutter.dart
+ test_pigeon_dart ./pigeons/voidhost.dart
+}
+
+run_java_compilation_tests() {
+ # Make sure the artifacts are present.
+ flutter precache
+ # Make sure flutter dependencies are available.
+ pushd $PWD
+ # We use e2e_tests/test_objc in order to get access to Flutter.
+ cd e2e_tests/test_objc/
+ flutter pub get
+ popd
+ test_pigeon_android ./pigeons/all_datatypes.dart
+ test_pigeon_android ./pigeons/async_handlers.dart
+ test_pigeon_android ./pigeons/host2flutter.dart
+ test_pigeon_android ./pigeons/java_double_host_api.dart
+ test_pigeon_android ./pigeons/list.dart
+ test_pigeon_android ./pigeons/message.dart
+ test_pigeon_android ./pigeons/void_arg_flutter.dart
+ test_pigeon_android ./pigeons/void_arg_host.dart
+ test_pigeon_android ./pigeons/voidflutter.dart
+ test_pigeon_android ./pigeons/voidhost.dart
+}
+
+run_objc_compilation_tests() {
+ # Make sure the artifacts are present.
+ flutter precache
+
+ test_pigeon_ios ./pigeons/all_datatypes.dart
+ test_pigeon_ios ./pigeons/async_handlers.dart
+ test_pigeon_ios ./pigeons/host2flutter.dart
+ test_pigeon_ios ./pigeons/list.dart
+ test_pigeon_ios ./pigeons/message.dart
+ test_pigeon_ios ./pigeons/void_arg_flutter.dart
+ test_pigeon_ios ./pigeons/void_arg_host.dart
+ test_pigeon_ios ./pigeons/voidflutter.dart
+ test_pigeon_ios ./pigeons/voidhost.dart
+}
+
+run_ios_unittests() {
+ pub run pigeon \
+ --no-dart_null_safety \
+ --input pigeons/message.dart \
+ --dart_out /dev/null \
+ --objc_header_out platform_tests/ios_unit_tests/ios/Runner/messages.h \
+ --objc_source_out platform_tests/ios_unit_tests/ios/Runner/messages.m
+ pub run pigeon \
+ --no-dart_null_safety \
+ --input pigeons/async_handlers.dart \
+ --dart_out /dev/null \
+ --objc_header_out platform_tests/ios_unit_tests/ios/Runner/async_handlers.h \
+ --objc_source_out platform_tests/ios_unit_tests/ios/Runner/async_handlers.m
+ clang-format -i platform_tests/ios_unit_tests/ios/Runner/messages.h
+ clang-format -i platform_tests/ios_unit_tests/ios/Runner/messages.m
+ clang-format -i platform_tests/ios_unit_tests/ios/Runner/async_handlers.h
+ clang-format -i platform_tests/ios_unit_tests/ios/Runner/async_handlers.m
+ pushd $PWD
+ cd platform_tests/ios_unit_tests
+ flutter build ios --simulator
+ cd ios
+ xcodebuild \
+ -workspace Runner.xcworkspace \
+ -scheme RunnerTests \
+ -sdk iphonesimulator \
+ -destination 'platform=iOS Simulator,name=iPhone 8' \
+ test
+ popd
+}
+
+run_ios_e2e_tests() {
+ DARTLE_H="e2e_tests/test_objc/ios/Runner/dartle.h"
+ DARTLE_M="e2e_tests/test_objc/ios/Runner/dartle.m"
+ DARTLE_DART="e2e_tests/test_objc/lib/dartle.dart"
+ PIGEON_JAVA="e2e_tests/test_objc/android/app/src/main/java/io/flutter/plugins/Pigeon.java"
+ pub run pigeon \
+ --input pigeons/message.dart \
+ --dart_out $DARTLE_DART \
+ --objc_header_out $DARTLE_H \
+ --objc_source_out $DARTLE_M \
+ --java_out $PIGEON_JAVA
+ dartfmt -w $DARTLE_DART
+
+ pushd $PWD
+ cd e2e_tests/test_objc
+ flutter build ios -t test_driver/e2e_test.dart --simulator
+ cd ios
+ xcodebuild \
+ -workspace Runner.xcworkspace \
+ -scheme RunnerTests \
+ -sdk iphonesimulator \
+ -destination 'platform=iOS Simulator,name=iPhone 8' \
+ test
+ popd
+}
+
+run_formatter() {
+ cd ../..
+ pub global activate flutter_plugin_tools && pub global run flutter_plugin_tools format 2>/dev/null
+}
+
+run_android_unittests() {
+ pushd $PWD
+ pub run pigeon \
+ --input pigeons/android_unittests.dart \
+ --dart_out /dev/null \
+ --java_out platform_tests/android_unit_tests/android/app/src/main/java/com/example/android_unit_tests/Pigeon.java \
+ --java_package "com.example.android_unit_tests"
+
+ cd platform_tests/android_unit_tests
+ if [ ! -f "android/gradlew" ]; then
+ flutter build apk --debug
+ fi
+ cd android
+ ./gradlew test
+ popd
+}
+
+###############################################################################
+# main
+###############################################################################
+should_run_android_unittests=true
+should_run_dart_compilation_tests=true
+should_run_dart_unittests=true
+should_run_flutter_unittests=true
+should_run_formatter=true
+should_run_ios_e2e_tests=true
+should_run_ios_unittests=true
+should_run_java_compilation_tests=true
+should_run_mock_handler_tests=true
+should_run_objc_compilation_tests=true
+while getopts "t:l?h" opt; do
+ case $opt in
+ t)
+ should_run_android_unittests=false
+ should_run_dart_compilation_tests=false
+ should_run_dart_unittests=false
+ should_run_flutter_unittests=false
+ should_run_formatter=false
+ should_run_ios_e2e_tests=false
+ should_run_ios_unittests=false
+ should_run_java_compilation_tests=false
+ should_run_mock_handler_tests=false
+ should_run_objc_compilation_tests=false
+ case $OPTARG in
+ android_unittests) should_run_android_unittests=true ;;
+ dart_compilation_tests) should_run_dart_compilation_tests=true ;;
+ dart_unittests) should_run_dart_unittests=true ;;
+ flutter_unittests) should_run_flutter_unittests=true ;;
+ ios_e2e_tests) should_run_ios_e2e_tests=true ;;
+ ios_unittests) should_run_ios_unittests=true ;;
+ java_compilation_tests) should_run_java_compilation_tests=true ;;
+ mock_handler_tests) should_run_mock_handler_tests=true ;;
+ objc_compilation_tests) should_run_objc_compilation_tests=true ;;
+ *)
+ echo "unrecognized test: $OPTARG"
+ exit 1
+ ;;
+ esac
+ ;;
+ l)
+ echo "available tests for -t:
+ android_unittests - Unit tests on generated Java code.
+ dart_compilation_tests - Compilation tests on generated Dart code.
+ dart_unittests - Unit tests on and analysis on Pigeon's implementation.
+ flutter_unittests - Unit tests on generated Dart code.
+ ios_e2e_tests - End-to-end objc tests run on iOS Simulator
+ ios_unittests - Unit tests on generated Objc code.
+ java_compilation_tests - Compilation tests on generated Java code.
+ mock_handler_tests - Unit tests on generated Dart mock handler code.
+ objc_compilation_tests - Compilation tests on generated Objc code.
+ "
+ exit 1
+ ;;
+ \h)
+ print_usage
+ exit 1
+ ;;
+ \?)
+ print_usage
+ exit 1
+ ;;
+ ?)
+ print_usage
+ exit 1
+ ;;
+ esac
+done
+
+if [ "$should_run_java_compilation_tests" = true ]; then
+ get_java_linter_formatter
+fi
+pub get
+test_running_without_arguments
+if [ "$should_run_dart_unittests" = true ]; then
+ run_dart_unittests
+fi
+if [ "$should_run_flutter_unittests" = true ]; then
+ run_flutter_unittests
+fi
+if [ "$should_run_mock_handler_tests" = true ]; then
+ run_mock_handler_tests
+fi
+if [ "$should_run_dart_compilation_tests" = true ]; then
+ run_dart_compilation_tests
+fi
+if [ "$should_run_java_compilation_tests" = true ]; then
+ run_java_compilation_tests
+fi
+if [ "$should_run_objc_compilation_tests" = true ]; then
+ run_objc_compilation_tests
+fi
+if [ "$should_run_ios_unittests" = true ]; then
+ run_ios_unittests
+fi
+if [ "$should_run_ios_e2e_tests" = true ]; then
+ run_ios_e2e_tests
+fi
+if [ "$should_run_android_unittests" = true ]; then
+ run_android_unittests
+fi
+if [ "$should_run_formatter" = true ]; then
+ run_formatter
+fi
diff --git a/packages/pigeon/test/dart_generator_test.dart b/packages/pigeon/test/dart_generator_test.dart
new file mode 100644
index 0000000..5463793
--- /dev/null
+++ b/packages/pigeon/test/dart_generator_test.dart
@@ -0,0 +1,368 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/ast.dart';
+import 'package:pigeon/dart_generator.dart';
+import 'package:pigeon/generator_tools.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('gen one class', () {
+ final Class klass = Class(
+ name: 'Foobar',
+ fields: <Field>[
+ Field(
+ name: 'field1',
+ dataType: 'dataType1',
+ ),
+ ],
+ );
+ final Root root = Root(
+ apis: <Api>[],
+ classes: <Class>[klass],
+ );
+ final StringBuffer sink = StringBuffer();
+ generateDart(DartOptions(isNullSafe: false), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('class Foobar'));
+ expect(code, contains(' dataType1 field1;'));
+ });
+
+ test('gen one host api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'Output',
+ isAsynchronous: false,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateDart(DartOptions(isNullSafe: false), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('class Api'));
+ expect(code, matches('Output.*doSomething.*Input'));
+ });
+
+ test('nested class', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')],
+ ),
+ Class(
+ name: 'Nested',
+ fields: <Field>[Field(name: 'nested', dataType: 'Input')],
+ )
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateDart(DartOptions(isNullSafe: false), root, sink);
+ final String code = sink.toString();
+ expect(
+ code,
+ contains(
+ 'pigeonMap[\'nested\'] = nested == null ? null : nested.encode()',
+ ),
+ );
+ expect(
+ code.replaceAll('\n', ' ').replaceAll(' ', ''),
+ contains(
+ '..nested = pigeonMap[\'nested\'] != null ? Input.decode(pigeonMap[\'nested\']) : null;',
+ ),
+ );
+ });
+
+ test('flutterapi', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'Output',
+ isAsynchronous: false,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateDart(DartOptions(isNullSafe: false), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('abstract class Api'));
+ expect(code, contains('static void setup(Api'));
+ });
+
+ test('host void', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'void',
+ isAsynchronous: false,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateDart(DartOptions(isNullSafe: false), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('Future<void> doSomething'));
+ expect(code, contains('// noop'));
+ });
+
+ test('flutter void return', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'void',
+ isAsynchronous: false,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateDart(DartOptions(isNullSafe: false), root, sink);
+ final String code = sink.toString();
+ // The next line verifies that we're not setting a variable to the value of "doSomething", but
+ // ignores the line where we assert the value of the argument isn't null, since on that line
+ // we mention "doSomething" in the assertion message.
+ expect(code, isNot(matches('[^!]=.*doSomething')));
+ expect(code, contains('doSomething('));
+ expect(code, isNot(contains('.encode()')));
+ });
+
+ test('flutter void argument', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'void',
+ returnType: 'Output',
+ isAsynchronous: false,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateDart(DartOptions(isNullSafe: false), root, sink);
+ final String code = sink.toString();
+ expect(code, matches('output.*=.*doSomething[(][)]'));
+ expect(code, contains('Output doSomething();'));
+ });
+
+ test('host void argument', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'void',
+ returnType: 'Output',
+ isAsynchronous: false,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateDart(DartOptions(isNullSafe: false), root, sink);
+ final String code = sink.toString();
+ expect(code, matches('channel.send[(]null[)]'));
+ });
+
+ test('mock dart handler', () {
+ final Root root = Root(apis: <Api>[
+ Api(
+ name: 'Api',
+ location: ApiLocation.host,
+ dartHostTestHandler: 'ApiMock',
+ methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'Output',
+ isAsynchronous: false,
+ ),
+ Method(
+ name: 'voidReturner',
+ argType: 'Input',
+ returnType: 'void',
+ isAsynchronous: false,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')])
+ ]);
+ final StringBuffer mainCodeSink = StringBuffer();
+ final StringBuffer testCodeSink = StringBuffer();
+ generateDart(DartOptions(isNullSafe: false), root, mainCodeSink);
+ final String mainCode = mainCodeSink.toString();
+ expect(mainCode, isNot(contains('import \'fo\\\'o.dart\';')));
+ expect(mainCode, contains('class Api {'));
+ expect(mainCode, isNot(contains('abstract class ApiMock')));
+ expect(mainCode, isNot(contains('.ApiMock.doSomething')));
+ expect(mainCode, isNot(contains('\'${Keys.result}\': output.encode()')));
+ expect(mainCode, isNot(contains('return <Object, Object>{};')));
+ generateTestDart(
+ DartOptions(isNullSafe: false), root, testCodeSink, "fo'o.dart");
+ final String testCode = testCodeSink.toString();
+ expect(testCode, contains('import \'fo\\\'o.dart\';'));
+ expect(testCode, isNot(contains('class Api {')));
+ expect(testCode, contains('abstract class ApiMock'));
+ expect(testCode, isNot(contains('.ApiMock.doSomething')));
+ expect(testCode, contains('\'${Keys.result}\': output.encode()'));
+ expect(testCode, contains('return <Object, Object>{};'));
+ });
+
+ test('opt out of nndb', () {
+ final Class klass = Class(
+ name: 'Foobar',
+ fields: <Field>[
+ Field(
+ name: 'field1',
+ dataType: 'dataType1',
+ ),
+ ],
+ );
+ final Root root = Root(
+ apis: <Api>[],
+ classes: <Class>[klass],
+ );
+ final StringBuffer sink = StringBuffer();
+ generateDart(DartOptions(isNullSafe: false), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('// @dart = 2.8'));
+ });
+
+ test('gen one async Flutter Api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'Output',
+ isAsynchronous: true,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateDart(DartOptions(isNullSafe: false), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('abstract class Api'));
+ expect(code, contains('Future<Output> doSomething(Input arg);'));
+ expect(
+ code, contains('final Output output = await api.doSomething(input);'));
+ });
+
+ test('gen one async Flutter Api with void return', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'void',
+ isAsynchronous: true,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateDart(DartOptions(isNullSafe: false), root, sink);
+ final String code = sink.toString();
+ expect(code, isNot(matches('=.s*doSomething')));
+ expect(code, contains('await api.doSomething('));
+ expect(code, isNot(contains('._toMap()')));
+ });
+
+ test('gen one async Host Api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'Output',
+ isAsynchronous: true,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateDart(DartOptions(isNullSafe: false), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('class Api'));
+ expect(code, matches('Output.*doSomething.*Input'));
+ });
+
+ test('async host void argument', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'void',
+ returnType: 'Output',
+ isAsynchronous: true,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateDart(DartOptions(isNullSafe: false), root, sink);
+ final String code = sink.toString();
+ expect(code, matches('channel.send[(]null[)]'));
+ });
+}
diff --git a/packages/pigeon/test/java_generator_test.dart b/packages/pigeon/test/java_generator_test.dart
new file mode 100644
index 0000000..053049b
--- /dev/null
+++ b/packages/pigeon/test/java_generator_test.dart
@@ -0,0 +1,351 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/ast.dart';
+import 'package:pigeon/java_generator.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('gen one class', () {
+ final Class klass = Class(
+ name: 'Foobar',
+ fields: <Field>[
+ Field(
+ name: 'field1',
+ dataType: 'int',
+ ),
+ ],
+ );
+ final Root root = Root(
+ apis: <Api>[],
+ classes: <Class>[klass],
+ );
+ final StringBuffer sink = StringBuffer();
+ final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+ generateJava(javaOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('public class Messages'));
+ expect(code, contains('public static class Foobar'));
+ expect(code, contains('private Long field1;'));
+ });
+
+ test('package', () {
+ final Class klass = Class(
+ name: 'Foobar',
+ fields: <Field>[
+ Field(
+ name: 'field1',
+ dataType: 'int',
+ )
+ ],
+ );
+ final Root root = Root(
+ apis: <Api>[],
+ classes: <Class>[klass],
+ );
+ final StringBuffer sink = StringBuffer();
+ final JavaOptions javaOptions = JavaOptions(className: 'Messages')
+ ..package = 'com.google.foobar';
+ generateJava(javaOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('package com.google.foobar;'));
+ expect(code, contains('Map<String, Object> toMap()'));
+ });
+
+ test('gen one host api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'Output',
+ isAsynchronous: false,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+ generateJava(javaOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('public interface Api'));
+ expect(code, matches('Output.*doSomething.*Input'));
+ expect(code, contains('channel.setMessageHandler(null)'));
+ });
+
+ test('all the simple datatypes header', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(name: 'Foobar', fields: <Field>[
+ Field(name: 'aBool', dataType: 'bool'),
+ Field(name: 'aInt', dataType: 'int'),
+ Field(name: 'aDouble', dataType: 'double'),
+ Field(name: 'aString', dataType: 'String'),
+ Field(name: 'aUint8List', dataType: 'Uint8List'),
+ Field(name: 'aInt32List', dataType: 'Int32List'),
+ Field(name: 'aInt64List', dataType: 'Int64List'),
+ Field(name: 'aFloat64List', dataType: 'Float64List'),
+ ]),
+ ]);
+
+ final StringBuffer sink = StringBuffer();
+ final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+ generateJava(javaOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('private Boolean aBool;'));
+ expect(code, contains('private Long aInt;'));
+ expect(code, contains('private Double aDouble;'));
+ expect(code, contains('private String aString;'));
+ expect(code, contains('private byte[] aUint8List;'));
+ expect(code, contains('private int[] aInt32List;'));
+ expect(code, contains('private long[] aInt64List;'));
+ expect(code, contains('private double[] aFloat64List;'));
+ });
+
+ test('gen one flutter api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'Output',
+ isAsynchronous: false,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+ generateJava(javaOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('public static class Api'));
+ expect(code, matches('doSomething.*Input.*Output'));
+ });
+
+ test('gen host void api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'void',
+ isAsynchronous: false,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+ generateJava(javaOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, isNot(matches('=.*doSomething')));
+ expect(code, contains('doSomething('));
+ });
+
+ test('gen flutter void return api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'void',
+ isAsynchronous: false,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+ generateJava(javaOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('Reply<Void>'));
+ expect(code, isNot(contains('.fromMap(')));
+ expect(code, contains('callback.reply(null)'));
+ });
+
+ test('gen host void argument api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'void',
+ returnType: 'Output',
+ isAsynchronous: false,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+ generateJava(javaOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('Output doSomething()'));
+ expect(code, contains('api.doSomething()'));
+ });
+
+ test('gen flutter void argument api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'void',
+ returnType: 'Output',
+ isAsynchronous: false,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+ generateJava(javaOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('doSomething(Reply<Output>'));
+ expect(code, contains('channel.send(null'));
+ });
+
+ test('gen list', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(
+ name: 'Foobar',
+ fields: <Field>[Field(name: 'field1', dataType: 'List')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+ generateJava(javaOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('public static class Foobar'));
+ expect(code, contains('private List<Object> field1;'));
+ });
+
+ test('gen map', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(
+ name: 'Foobar',
+ fields: <Field>[Field(name: 'field1', dataType: 'Map')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+ generateJava(javaOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('public static class Foobar'));
+ expect(code, contains('private Map<Object, Object> field1;'));
+ });
+
+ test('gen nested', () {
+ final Class klass = Class(
+ name: 'Outer',
+ fields: <Field>[
+ Field(
+ name: 'nested',
+ dataType: 'Nested',
+ )
+ ],
+ );
+ final Class nestedClass = Class(
+ name: 'Nested',
+ fields: <Field>[
+ Field(
+ name: 'data',
+ dataType: 'int',
+ )
+ ],
+ );
+ final Root root = Root(
+ apis: <Api>[],
+ classes: <Class>[klass, nestedClass],
+ );
+ final StringBuffer sink = StringBuffer();
+ final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+ generateJava(javaOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('public class Messages'));
+ expect(code, contains('public static class Outer'));
+ expect(code, contains('public static class Nested'));
+ expect(code, contains('private Nested nested;'));
+ expect(code, contains('Nested.fromMap((Map)nested);'));
+ expect(code, contains('put("nested", nested.toMap());'));
+ });
+
+ test('gen one async Host Api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'Output',
+ isAsynchronous: true,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+ generateJava(javaOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('public interface Api'));
+ expect(code, contains('public interface Result<T> {'));
+ expect(
+ code, contains('void doSomething(Input arg, Result<Output> result);'));
+ expect(
+ code,
+ contains(
+ 'api.doSomething(input, result -> { wrapped.put("result", result.toMap()); reply.reply(wrapped); });'));
+ expect(code, contains('channel.setMessageHandler(null)'));
+ });
+
+ test('gen one async Flutter Api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'Output',
+ isAsynchronous: true,
+ )
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+ generateJava(javaOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('public static class Api'));
+ expect(code, matches('doSomething.*Input.*Output'));
+ });
+}
diff --git a/packages/pigeon/test/objc_generator_test.dart b/packages/pigeon/test/objc_generator_test.dart
new file mode 100644
index 0000000..329f151
--- /dev/null
+++ b/packages/pigeon/test/objc_generator_test.dart
@@ -0,0 +1,619 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:pigeon/objc_generator.dart';
+import 'package:pigeon/ast.dart';
+
+void main() {
+ test('gen one class header', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(
+ name: 'Foobar',
+ fields: <Field>[Field(name: 'field1', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@interface Foobar'));
+ expect(code, matches('@property.*NSString.*field1'));
+ });
+
+ test('gen one class source', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(
+ name: 'Foobar',
+ fields: <Field>[Field(name: 'field1', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('#import "foo.h"'));
+ expect(code, contains('@implementation Foobar'));
+ });
+
+ test('gen one api header', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(name: 'doSomething', argType: 'Input', returnType: 'Output')
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@interface Input'));
+ expect(code, contains('@interface Output'));
+ expect(code, contains('@protocol Api'));
+ expect(code, matches('nullable Output.*doSomething.*Input.*FlutterError'));
+ expect(code, matches('ApiSetup.*<Api>.*_Nullable'));
+ });
+
+ test('gen one api source', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(name: 'doSomething', argType: 'Input', returnType: 'Output')
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('#import "foo.h"'));
+ expect(code, contains('@implementation Input'));
+ expect(code, contains('@implementation Output'));
+ expect(code, contains('ApiSetup('));
+ });
+
+ test('all the simple datatypes header', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(name: 'Foobar', fields: <Field>[
+ Field(name: 'aBool', dataType: 'bool'),
+ Field(name: 'aInt', dataType: 'int'),
+ Field(name: 'aDouble', dataType: 'double'),
+ Field(name: 'aString', dataType: 'String'),
+ Field(name: 'aUint8List', dataType: 'Uint8List'),
+ Field(name: 'aInt32List', dataType: 'Int32List'),
+ Field(name: 'aInt64List', dataType: 'Int64List'),
+ Field(name: 'aFloat64List', dataType: 'Float64List'),
+ ]),
+ ]);
+
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(header: 'foo.h'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@interface Foobar'));
+ expect(code, contains('@class FlutterStandardTypedData;'));
+ expect(code, matches('@property.*strong.*NSNumber.*aBool'));
+ expect(code, matches('@property.*strong.*NSNumber.*aInt'));
+ expect(code, matches('@property.*strong.*NSNumber.*aDouble'));
+ expect(code, matches('@property.*copy.*NSString.*aString'));
+ expect(code,
+ matches('@property.*strong.*FlutterStandardTypedData.*aUint8List'));
+ expect(code,
+ matches('@property.*strong.*FlutterStandardTypedData.*aInt32List'));
+ expect(code,
+ matches('@property.*strong.*FlutterStandardTypedData.*Int64List'));
+ expect(code,
+ matches('@property.*strong.*FlutterStandardTypedData.*Float64List'));
+ });
+
+ test('bool source', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(name: 'Foobar', fields: <Field>[
+ Field(name: 'aBool', dataType: 'bool'),
+ ]),
+ ]);
+
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@implementation Foobar'));
+ expect(code, contains('result.aBool = dict[@"aBool"];'));
+ });
+
+ test('nested class header', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Nested',
+ fields: <Field>[Field(name: 'nested', dataType: 'Input')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(header: 'foo.h'), root, sink);
+ final String code = sink.toString();
+ expect(code,
+ contains('@property(nonatomic, strong, nullable) Input * nested;'));
+ });
+
+ test('nested class source', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Nested',
+ fields: <Field>[Field(name: 'nested', dataType: 'Input')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('result.nested = [Input fromMap:dict[@"nested"]];'));
+ expect(code, matches('[self.nested toMap].*@"nested"'));
+ });
+
+ test('prefix class header', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(
+ name: 'Foobar',
+ fields: <Field>[Field(name: 'field1', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@interface ABCFoobar'));
+ });
+
+ test('prefix class source', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(
+ name: 'Foobar',
+ fields: <Field>[Field(name: 'field1', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@implementation ABCFoobar'));
+ });
+
+ test('prefix nested class header', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(name: 'doSomething', argType: 'Input', returnType: 'Nested')
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Nested',
+ fields: <Field>[Field(name: 'nested', dataType: 'Input')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(code, matches('property.*ABCInput'));
+ expect(code, matches('ABCNested.*doSomething.*ABCInput'));
+ expect(code, contains('@protocol ABCApi'));
+ });
+
+ test('prefix nested class source', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(name: 'doSomething', argType: 'Input', returnType: 'Nested')
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Nested',
+ fields: <Field>[Field(name: 'nested', dataType: 'Input')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('ABCInput fromMap'));
+ expect(code, matches('ABCInput.*=.*ABCInput fromMap'));
+ expect(code, contains('void ABCApiSetup('));
+ });
+
+ test('gen flutter api header', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(name: 'doSomething', argType: 'Input', returnType: 'Output')
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(header: 'foo.h'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@interface Api : NSObject'));
+ expect(
+ code,
+ contains(
+ 'initWithBinaryMessenger:(id<FlutterBinaryMessenger>)binaryMessenger;'));
+ expect(code, matches('void.*doSomething.*Input.*Output'));
+ });
+
+ test('gen flutter api source', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(name: 'doSomething', argType: 'Input', returnType: 'Output')
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@implementation Api'));
+ expect(code, matches('void.*doSomething.*Input.*Output.*{'));
+ });
+
+ test('gen host void header', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(name: 'doSomething', argType: 'Input', returnType: 'void')
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('(void)doSomething:'));
+ });
+
+ test('gen host void source', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(name: 'doSomething', argType: 'Input', returnType: 'void')
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(code, isNot(matches('=.*doSomething')));
+ expect(code, matches('[.*doSomething:.*]'));
+ expect(code, contains('callback(wrapResult(nil, error))'));
+ });
+
+ test('gen flutter void return header', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(name: 'doSomething', argType: 'Input', returnType: 'void')
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('completion:(void(^)(NSError* _Nullable))'));
+ });
+
+ test('gen flutter void return source', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(name: 'doSomething', argType: 'Input', returnType: 'void')
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('completion:(void(^)(NSError* _Nullable))'));
+ expect(code, contains('completion(nil)'));
+ });
+
+ test('gen host void arg header', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(name: 'doSomething', argType: 'void', returnType: 'Output')
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(code, matches('ABCOutput.*doSomething:[(]FlutterError'));
+ });
+
+ test('gen host void arg source', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(name: 'doSomething', argType: 'void', returnType: 'Output')
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(code, matches('output.*=.*api doSomething:&error'));
+ });
+
+ test('gen flutter void arg header', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(name: 'doSomething', argType: 'void', returnType: 'Output')
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(
+ code,
+ contains(
+ '(void)doSomething:(void(^)(ABCOutput*, NSError* _Nullable))completion'));
+ });
+
+ test('gen flutter void arg header', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(name: 'doSomething', argType: 'void', returnType: 'Output')
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(
+ code,
+ contains(
+ '(void)doSomething:(void(^)(ABCOutput*, NSError* _Nullable))completion'));
+ expect(code, contains('channel sendMessage:nil'));
+ });
+
+ test('gen list', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(
+ name: 'Foobar',
+ fields: <Field>[Field(name: 'field1', dataType: 'List')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@interface Foobar'));
+ expect(code, matches('@property.*NSArray.*field1'));
+ });
+
+ test('gen map', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(
+ name: 'Foobar',
+ fields: <Field>[Field(name: 'field1', dataType: 'Map')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@interface Foobar'));
+ expect(code, matches('@property.*NSDictionary.*field1'));
+ });
+
+ test('async void(input) HostApi header', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'void',
+ isAsynchronous: true)
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(
+ code,
+ contains(
+ '(void)doSomething:(nullable ABCInput *)input completion:(void(^)(FlutterError *_Nullable))completion'));
+ });
+
+ test('async output(input) HostApi header', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'Output',
+ isAsynchronous: true)
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(
+ code,
+ contains(
+ '(void)doSomething:(nullable ABCInput *)input completion:(void(^)(ABCOutput *_Nullable, FlutterError *_Nullable))completion'));
+ });
+
+ test('async output(void) HostApi header', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'void',
+ returnType: 'Output',
+ isAsynchronous: true)
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(
+ code,
+ contains(
+ '(void)doSomething:(void(^)(ABCOutput *_Nullable, FlutterError *_Nullable))completion'));
+ });
+
+ test('async void(void) HostApi header', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'void',
+ returnType: 'void',
+ isAsynchronous: true)
+ ])
+ ], classes: <Class>[]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(
+ code,
+ contains(
+ '(void)doSomething:(void(^)(FlutterError *_Nullable))completion'));
+ });
+
+ test('async output(input) HostApi source', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'Output',
+ isAsynchronous: true)
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(
+ code,
+ contains(
+ '[api doSomething:input completion:^(ABCOutput *_Nullable output, FlutterError *_Nullable error) {'));
+ });
+
+ test('async void(input) HostApi source', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'Input',
+ returnType: 'void',
+ isAsynchronous: true)
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Input',
+ fields: <Field>[Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(
+ code,
+ contains(
+ '[api doSomething:input completion:^(FlutterError *_Nullable error) {'));
+ });
+
+ test('async void(void) HostApi source', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'void',
+ returnType: 'void',
+ isAsynchronous: true)
+ ])
+ ], classes: <Class>[]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(
+ code, contains('[api doSomething:^(FlutterError *_Nullable error) {'));
+ });
+
+ test('async output(void) HostApi source', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ argType: 'void',
+ returnType: 'Output',
+ isAsynchronous: true)
+ ])
+ ], classes: <Class>[
+ Class(
+ name: 'Output',
+ fields: <Field>[Field(name: 'output', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(
+ code,
+ contains(
+ '[api doSomething:^(ABCOutput *_Nullable output, FlutterError *_Nullable error) {'));
+ });
+}
diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart
new file mode 100644
index 0000000..f92117d
--- /dev/null
+++ b/packages/pigeon/test/pigeon_lib_test.dart
@@ -0,0 +1,246 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:pigeon/pigeon_lib.dart';
+import 'package:pigeon/ast.dart';
+
+class Input1 {
+ String? input;
+}
+
+class Output1 {
+ String? output;
+}
+
+@HostApi()
+abstract class Api1 {
+ Output1 doit(Input1 input);
+}
+
+class InvalidDatatype {
+ dynamic something;
+}
+
+@HostApi()
+abstract class ApiTwoMethods {
+ Output1 method1(Input1 input);
+ Output1 method2(Input1 input);
+}
+
+class Nested {
+ Input1? input;
+}
+
+@FlutterApi()
+abstract class AFlutterApi {
+ Output1 doit(Input1 input);
+}
+
+@HostApi()
+abstract class VoidApi {
+ void doit(Input1 input);
+}
+
+@HostApi()
+abstract class VoidArgApi {
+ Output1 doit();
+}
+
+@HostApi(dartHostTestHandler: 'ApiWithMockDartClassMock')
+abstract class ApiWithMockDartClass {
+ Output1 doit();
+}
+
+class OnlyVisibleFromNesting {
+ String? foo;
+}
+
+class Nestor {
+ OnlyVisibleFromNesting? nested;
+}
+
+@HostApi()
+abstract class NestorApi {
+ Nestor getit();
+}
+
+@HostApi()
+abstract class InvalidArgTypeApi {
+ void doit(bool value);
+}
+
+@HostApi()
+abstract class InvalidReturnTypeApi {
+ bool doit();
+}
+
+void main() {
+ test('parse args - input', () {
+ final PigeonOptions opts =
+ Pigeon.parseArgs(<String>['--input', 'foo.dart']);
+ expect(opts.input, equals('foo.dart'));
+ });
+
+ test('parse args - dart_out', () {
+ final PigeonOptions opts =
+ Pigeon.parseArgs(<String>['--dart_out', 'foo.dart']);
+ expect(opts.dartOut, equals('foo.dart'));
+ });
+
+ test('parse args - java_package', () {
+ final PigeonOptions opts =
+ Pigeon.parseArgs(<String>['--java_package', 'com.google.foo']);
+ expect(opts.javaOptions?.package, equals('com.google.foo'));
+ });
+
+ test('parse args - input', () {
+ final PigeonOptions opts =
+ Pigeon.parseArgs(<String>['--java_out', 'foo.java']);
+ expect(opts.javaOut, equals('foo.java'));
+ });
+
+ test('parse args - objc_header_out', () {
+ final PigeonOptions opts =
+ Pigeon.parseArgs(<String>['--objc_header_out', 'foo.h']);
+ expect(opts.objcHeaderOut, equals('foo.h'));
+ });
+
+ test('parse args - objc_source_out', () {
+ final PigeonOptions opts =
+ Pigeon.parseArgs(<String>['--objc_source_out', 'foo.m']);
+ expect(opts.objcSourceOut, equals('foo.m'));
+ });
+
+ test('simple parse api', () {
+ final Pigeon dartle = Pigeon.setup();
+ final ParseResults parseResult = dartle.parse(<Type>[Api1]);
+ expect(parseResult.errors.length, equals(0));
+ final Root root = parseResult.root;
+ expect(root.classes.length, equals(2));
+ expect(root.apis.length, equals(1));
+ expect(root.apis[0].name, equals('Api1'));
+ expect(root.apis[0].methods.length, equals(1));
+ expect(root.apis[0].methods[0].name, equals('doit'));
+ expect(root.apis[0].methods[0].argType, equals('Input1'));
+ expect(root.apis[0].methods[0].returnType, equals('Output1'));
+
+ Class? input;
+ Class? output;
+ for (final Class klass in root.classes) {
+ if (klass.name == 'Input1') {
+ input = klass;
+ } else if (klass.name == 'Output1') {
+ output = klass;
+ }
+ }
+ expect(input, isNotNull);
+ expect(output, isNotNull);
+
+ expect(input?.fields.length, equals(1));
+ expect(input?.fields[0].name, equals('input'));
+ expect(input?.fields[0].dataType, equals('String'));
+
+ expect(output?.fields.length, equals(1));
+ expect(output?.fields[0].name, equals('output'));
+ expect(output?.fields[0].dataType, equals('String'));
+ });
+
+ test('invalid datatype', () {
+ final Pigeon dartle = Pigeon.setup();
+ final ParseResults results = dartle.parse(<Type>[InvalidDatatype]);
+ expect(results.errors.length, 1);
+ expect(results.errors[0].message, contains('InvalidDatatype'));
+ expect(results.errors[0].message, contains('dynamic'));
+ });
+
+ test('two methods', () {
+ final Pigeon dartle = Pigeon.setup();
+ final ParseResults results = dartle.parse(<Type>[ApiTwoMethods]);
+ expect(results.errors.length, 0);
+ expect(results.root.apis.length, 1);
+ expect(results.root.apis[0].methods.length, equals(2));
+ expect(results.root.apis[0].methods[0].name, equals('method1'));
+ expect(results.root.apis[0].methods[1].name, equals('method2'));
+ });
+
+ test('nested', () {
+ final Pigeon dartle = Pigeon.setup();
+ final ParseResults results = dartle.parse(<Type>[Nested, Input1]);
+ expect(results.errors.length, equals(0));
+ expect(results.root.classes.length, equals(2));
+ expect(results.root.classes[0].name, equals('Nested'));
+ expect(results.root.classes[0].fields.length, equals(1));
+ expect(results.root.classes[0].fields[0].dataType, equals('Input1'));
+ });
+
+ test('flutter api', () {
+ final Pigeon pigeon = Pigeon.setup();
+ final ParseResults results = pigeon.parse(<Type>[AFlutterApi]);
+ expect(results.errors.length, equals(0));
+ expect(results.root.apis.length, equals(1));
+ expect(results.root.apis[0].name, equals('AFlutterApi'));
+ expect(results.root.apis[0].location, equals(ApiLocation.flutter));
+ });
+
+ test('void host api', () {
+ final Pigeon pigeon = Pigeon.setup();
+ final ParseResults results = pigeon.parse(<Type>[VoidApi]);
+ expect(results.errors.length, equals(0));
+ expect(results.root.apis.length, equals(1));
+ expect(results.root.apis[0].methods.length, equals(1));
+ expect(results.root.apis[0].name, equals('VoidApi'));
+ expect(results.root.apis[0].methods[0].returnType, equals('void'));
+ });
+
+ test('void arg host api', () {
+ final Pigeon pigeon = Pigeon.setup();
+ final ParseResults results = pigeon.parse(<Type>[VoidArgApi]);
+ expect(results.errors.length, equals(0));
+ expect(results.root.apis.length, equals(1));
+ expect(results.root.apis[0].methods.length, equals(1));
+ expect(results.root.apis[0].name, equals('VoidArgApi'));
+ expect(results.root.apis[0].methods[0].returnType, equals('Output1'));
+ expect(results.root.apis[0].methods[0].argType, equals('void'));
+ });
+
+ test('mockDartClass', () {
+ final Pigeon pigeon = Pigeon.setup();
+ final ParseResults results = pigeon.parse(<Type>[ApiWithMockDartClass]);
+ expect(results.errors.length, equals(0));
+ expect(results.root.apis.length, equals(1));
+ expect(results.root.apis[0].dartHostTestHandler,
+ equals('ApiWithMockDartClassMock'));
+ });
+
+ test('only visible from nesting', () {
+ final Pigeon dartle = Pigeon.setup();
+ final ParseResults results = dartle.parse(<Type>[NestorApi]);
+ expect(results.errors.length, 0);
+ expect(results.root.apis.length, 1);
+ final List<String> classNames =
+ results.root.classes.map((Class x) => x.name).toList();
+ expect(classNames.length, 2);
+ expect(classNames.contains('Nestor'), true);
+ expect(classNames.contains('OnlyVisibleFromNesting'), true);
+ });
+
+ test('invalid datatype for argument', () {
+ final Pigeon pigeon = Pigeon.setup();
+ final ParseResults results = pigeon.parse(<Type>[InvalidArgTypeApi]);
+ expect(results.errors.length, 1);
+ });
+
+ test('invalid datatype for argument', () {
+ final Pigeon pigeon = Pigeon.setup();
+ final ParseResults results = pigeon.parse(<Type>[InvalidReturnTypeApi]);
+ expect(results.errors.length, 1);
+ });
+
+ test('null safety flag', () {
+ final PigeonOptions results =
+ Pigeon.parseArgs(<String>['--dart_null_safety']);
+ expect(results.dartOptions?.isNullSafe, isTrue);
+ });
+}
diff --git a/packages/pigeon/test/version_test.dart b/packages/pigeon/test/version_test.dart
new file mode 100644
index 0000000..1487b90
--- /dev/null
+++ b/packages/pigeon/test/version_test.dart
@@ -0,0 +1,19 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:pigeon/generator_tools.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('pigeon version matches pubspec', () {
+ final String pubspecPath = '${Directory.current.path}/pubspec.yaml';
+ final String pubspec = File(pubspecPath).readAsStringSync();
+ final RegExp regex = RegExp(r'version:\s*(.*?) #');
+ final RegExpMatch? match = regex.firstMatch(pubspec);
+ expect(match, isNotNull);
+ expect(pigeonVersion, match?.group(1)?.trim());
+ });
+}
diff --git a/packages/pointer_interceptor/.gitignore b/packages/pointer_interceptor/.gitignore
new file mode 100644
index 0000000..1985397
--- /dev/null
+++ b/packages/pointer_interceptor/.gitignore
@@ -0,0 +1,74 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+build/
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Flutter.podspec
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
diff --git a/packages/pointer_interceptor/.metadata b/packages/pointer_interceptor/.metadata
new file mode 100644
index 0000000..b533074
--- /dev/null
+++ b/packages/pointer_interceptor/.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: e6bd95bc5caa5e34c5b0285a559673984374b7ea
+ channel: master
+
+project_type: package
diff --git a/packages/pointer_interceptor/CHANGELOG.md b/packages/pointer_interceptor/CHANGELOG.md
new file mode 100644
index 0000000..06e0c6e
--- /dev/null
+++ b/packages/pointer_interceptor/CHANGELOG.md
@@ -0,0 +1,15 @@
+## 0.9.0
+
+* Migrates to null safety.
+
+## 0.8.0+2
+
+* Use `ElevatedButton` instead of the deprecated `RaisedButton` in example and docs.
+
+## 0.8.0+1
+
+* Update README.md so images render in pub.dev
+
+## 0.8.0
+
+* Initial release of the `PointerInterceptor` widget.
diff --git a/packages/pointer_interceptor/LICENSE b/packages/pointer_interceptor/LICENSE
new file mode 100644
index 0000000..73e6b6e
--- /dev/null
+++ b/packages/pointer_interceptor/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2019 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/pointer_interceptor/README.md b/packages/pointer_interceptor/README.md
new file mode 100644
index 0000000..bcd3b06
--- /dev/null
+++ b/packages/pointer_interceptor/README.md
@@ -0,0 +1,71 @@
+# pointer_interceptor
+
+`PointerInterceptor` is a widget that prevents mouse events (in web) from being captured by an underlying [`HtmlElementView`](https://api.flutter.dev/flutter/widgets/HtmlElementView-class.html).
+
+You can use this widget in a cross-platform app freely. In mobile, where the issue that this plugin fixes does not exist, the widget acts as a pass-through of its `children`, without adding anything to the render tree.
+
+## What is the problem?
+
+When overlaying Flutter widgets on top of `HtmlElementView` widgets that respond to mouse gestures (handle clicks, for example), the clicks will be consumed by the `HtmlElementView`, and not relayed to Flutter.
+
+The result is that Flutter widget's `onTap` (and other) handlers won't fire as expected, but they'll affect the underlying webview.
+
+|The problem...|
+|:-:|
+||
+|_In the dashed areas, mouse events won't work as expected. The `HtmlElementView` will consume them before Flutter sees them._|
+
+
+## How does this work?
+
+`PointerInterceptor` creates a platform view consisting of an empty HTML element. The element has the size of its `child` widget, and is inserted in the layer tree _behind_ its child in paint order.
+
+This empty platform view doesn't do anything with mouse events, other than preventing them from reaching other platform views underneath it.
+
+This gives an opportunity to the Flutter framework to handle the click, as expected:
+
+|The solution...|
+|:-:|
+||
+|_Each `PointerInterceptor` (green) renders between Flutter widgets and the underlying `HtmlElementView`. Mouse events now can't reach the background HtmlElementView, and work as expected._|
+
+## How to use
+
+Some common scenarios where this widget may come in handy:
+
+* [FAB](https://api.flutter.dev/flutter/material/FloatingActionButton-class.html) unclickable in an app that renders a full-screen background Map
+* Custom Play/Pause buttons on top of a video element don't work
+* Drawer contents not interactive when it overlaps an iframe element
+* ...
+
+All the cases above have in common that they attempt to render Flutter widgets *on top* of platform views that handle pointer events.
+
+There's two ways that the `PointerInterceptor` widget can be used to solve the problems above:
+
+1. Wrapping your button element directly (FAB, Custom Play/Pause button...):
+
+ ```dart
+ PointerInterceptor(
+ child: ElevatedButton(...),
+ )
+ ```
+
+2. As a root container for a "layout" element, wrapping a bunch of other elements (like a Drawer):
+
+ ```dart
+ Scaffold(
+ ...
+ drawer: PointerInterceptor(
+ child: Drawer(
+ child: ...
+ ),
+ ),
+ ...
+ )
+ ```
+
+### `debug`
+
+The `PointerInterceptor` widget has a `debug` property, that will render it visibly on the screen (similar to the images above).
+
+This may be useful to see what the widget is actually covering when used as a layout element.
diff --git a/packages/pointer_interceptor/doc/img/affected-areas.png b/packages/pointer_interceptor/doc/img/affected-areas.png
new file mode 100644
index 0000000..40592db
--- /dev/null
+++ b/packages/pointer_interceptor/doc/img/affected-areas.png
Binary files differ
diff --git a/packages/pointer_interceptor/doc/img/fixed-areas.png b/packages/pointer_interceptor/doc/img/fixed-areas.png
new file mode 100644
index 0000000..489d376
--- /dev/null
+++ b/packages/pointer_interceptor/doc/img/fixed-areas.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/.gitignore b/packages/pointer_interceptor/example/.gitignore
new file mode 100644
index 0000000..0fa6b67
--- /dev/null
+++ b/packages/pointer_interceptor/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/pointer_interceptor/example/.metadata b/packages/pointer_interceptor/example/.metadata
new file mode 100644
index 0000000..d2885b8
--- /dev/null
+++ b/packages/pointer_interceptor/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: e6bd95bc5caa5e34c5b0285a559673984374b7ea
+ channel: master
+
+project_type: app
diff --git a/packages/pointer_interceptor/example/README.md b/packages/pointer_interceptor/example/README.md
new file mode 100644
index 0000000..9fddf8c
--- /dev/null
+++ b/packages/pointer_interceptor/example/README.md
@@ -0,0 +1,17 @@
+# pointer_interceptor_example
+
+An example for the PointerInterceptor widget.
+
+## Getting Started
+
+`flutter run -d chrome` to run the sample. You can tweak some code in the `lib/main.dart`, but be careful, changes there can break integration tests!
+
+## Running tests
+
+`flutter drive --target integration_test/widget_test.dart --driver test_driver/integration_test.dart --show-web-server-device -d web-server --web-renderer=html`
+
+The command above will run the integration tests for this package.
+
+Make sure that you have `chromedriver` running in port `4444`.
+
+Read more on: [flutter.dev > Docs > Testing & debugging > Integration testing](https://flutter.dev/docs/testing/integration-tests).
diff --git a/packages/pointer_interceptor/example/android/.gitignore b/packages/pointer_interceptor/example/android/.gitignore
new file mode 100644
index 0000000..0a741cb
--- /dev/null
+++ b/packages/pointer_interceptor/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/pointer_interceptor/example/android/app/build.gradle b/packages/pointer_interceptor/example/android/app/build.gradle
new file mode 100644
index 0000000..3932aa9
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/build.gradle
@@ -0,0 +1,63 @@
+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 29
+
+ 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.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 '../..'
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}
diff --git a/packages/pointer_interceptor/example/android/app/src/debug/AndroidManifest.xml b/packages/pointer_interceptor/example/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..c208884
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.example">
+ <!-- Flutter needs it to communicate with the running application
+ to allow setting breakpoints, to provide hot reload, etc.
+ -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
diff --git a/packages/pointer_interceptor/example/android/app/src/main/AndroidManifest.xml b/packages/pointer_interceptor/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..34dd77e
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.example">
+ <application
+ android:label="example"
+ android:icon="@mipmap/ic_launcher">
+ <activity
+ android:name=".MainActivity"
+ android:launchMode="singleTop"
+ android:theme="@style/LaunchTheme"
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+ android:hardwareAccelerated="true"
+ android:windowSoftInputMode="adjustResize">
+ <!-- Specifies an Android theme to apply to this Activity as soon as
+ the Android process has started. This theme is visible to the user
+ while the Flutter UI initializes. After that, this theme continues
+ to determine the Window background behind the Flutter UI. -->
+ <meta-data
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme"
+ />
+ <!-- Displays an Android View that continues showing the launch screen
+ Drawable until Flutter paints its first frame, then this splash
+ screen fades out. A splash screen is useful to avoid any visual
+ gap between the end of Android's launch screen and the painting of
+ Flutter's first frame. -->
+ <meta-data
+ android:name="io.flutter.embedding.android.SplashScreenDrawable"
+ android:resource="@drawable/launch_background"
+ />
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <!-- Don't delete the meta-data below.
+ This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+ <meta-data
+ android:name="flutterEmbedding"
+ android:value="2" />
+ </application>
+</manifest>
diff --git a/packages/pointer_interceptor/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/packages/pointer_interceptor/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
new file mode 100644
index 0000000..e793a00
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.example.example
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/pointer_interceptor/example/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="?android:colorBackground" />
+
+ <!-- You can insert your own image assets here -->
+ <!-- <item>
+ <bitmap
+ android:gravity="center"
+ android:src="@mipmap/launch_image" />
+ </item> -->
+</layer-list>
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/drawable/launch_background.xml b/packages/pointer_interceptor/example/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@android:color/white" />
+
+ <!-- You can insert your own image assets here -->
+ <!-- <item>
+ <bitmap
+ android:gravity="center"
+ android:src="@mipmap/launch_image" />
+ </item> -->
+</layer-list>
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/values-night/styles.xml b/packages/pointer_interceptor/example/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..449a9f9
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
+ <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+ <!-- Show a splash screen on the activity. Automatically removed when
+ Flutter draws its first frame -->
+ <item name="android:windowBackground">@drawable/launch_background</item>
+ </style>
+ <!-- Theme applied to the Android Window as soon as the process has started.
+ This theme determines the color of the Android Window while your
+ Flutter UI initializes, as well as behind your Flutter UI while its
+ running.
+
+ This Theme is only used starting with V2 of Flutter's Android embedding. -->
+ <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+ <item name="android:windowBackground">?android:colorBackground</item>
+ </style>
+</resources>
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/values/styles.xml b/packages/pointer_interceptor/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..d74aa35
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
+ <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
+ <!-- Show a splash screen on the activity. Automatically removed when
+ Flutter draws its first frame -->
+ <item name="android:windowBackground">@drawable/launch_background</item>
+ </style>
+ <!-- Theme applied to the Android Window as soon as the process has started.
+ This theme determines the color of the Android Window while your
+ Flutter UI initializes, as well as behind your Flutter UI while its
+ running.
+
+ This Theme is only used starting with V2 of Flutter's Android embedding. -->
+ <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
+ <item name="android:windowBackground">?android:colorBackground</item>
+ </style>
+</resources>
diff --git a/packages/pointer_interceptor/example/android/app/src/profile/AndroidManifest.xml b/packages/pointer_interceptor/example/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..c208884
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.example">
+ <!-- Flutter needs it to communicate with the running application
+ to allow setting breakpoints, to provide hot reload, etc.
+ -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
diff --git a/packages/pointer_interceptor/example/android/build.gradle b/packages/pointer_interceptor/example/android/build.gradle
new file mode 100644
index 0000000..3100ad2
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/build.gradle
@@ -0,0 +1,31 @@
+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/pointer_interceptor/example/android/gradle.properties b/packages/pointer_interceptor/example/android/gradle.properties
new file mode 100644
index 0000000..94adc3a
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/pointer_interceptor/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/pointer_interceptor/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..296b146
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
diff --git a/packages/pointer_interceptor/example/android/settings.gradle b/packages/pointer_interceptor/example/android/settings.gradle
new file mode 100644
index 0000000..44e62bc
--- /dev/null
+++ b/packages/pointer_interceptor/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/pointer_interceptor/example/integration_test/widget_test.dart b/packages/pointer_interceptor/example/integration_test/widget_test.dart
new file mode 100644
index 0000000..72b02ad
--- /dev/null
+++ b/packages/pointer_interceptor/example/integration_test/widget_test.dart
@@ -0,0 +1,61 @@
+// 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 'dart:html' as html;
+
+// Imports the Flutter Driver API.
+import 'package:flutter/src/widgets/framework.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'package:pointer_interceptor_example/main.dart' as app;
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ group('Widget', () {
+ final Finder nonClickableButtonFinder =
+ find.byKey(const Key('transparent-button'));
+ final Finder clickableButtonFinder =
+ find.byKey(const Key('clickable-button'));
+
+ testWidgets(
+ 'on wrapped elements, the browser hits the interceptor (and not the background-html-view)',
+ (WidgetTester tester) async {
+ app.main();
+ await tester.pumpAndSettle();
+
+ final html.Element? element =
+ _getHtmlElementFromFinder(clickableButtonFinder, tester);
+ expect(element?.tagName.toLowerCase(), 'flt-platform-view');
+
+ final html.Element? platformViewRoot =
+ element?.shadowRoot?.getElementById('background-html-view');
+ expect(platformViewRoot, isNull);
+ });
+
+ testWidgets(
+ 'on unwrapped elements, the browser hits the background-html-view',
+ (WidgetTester tester) async {
+ app.main();
+ await tester.pumpAndSettle();
+
+ final html.Element? element =
+ _getHtmlElementFromFinder(nonClickableButtonFinder, tester);
+ expect(element?.tagName.toLowerCase(), 'flt-platform-view');
+
+ final html.Element? platformViewRoot =
+ element?.shadowRoot?.getElementById('background-html-view');
+ expect(platformViewRoot, isNotNull);
+ });
+ });
+}
+
+// This functions locates a widget from a Finder, and asks the browser what's the
+// DOM element in the center of the coordinates of the widget. (Returns *which*
+// DOM element will handle Mouse interactions first at those coordinates.)
+html.Element? _getHtmlElementFromFinder(Finder finder, WidgetTester tester) {
+ final Offset point = tester.getCenter(finder);
+ return html.document.elementFromPoint(point.dx.toInt(), point.dy.toInt());
+}
diff --git a/packages/pointer_interceptor/example/ios/.gitignore b/packages/pointer_interceptor/example/ios/.gitignore
new file mode 100644
index 0000000..e96ef60
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/.gitignore
@@ -0,0 +1,32 @@
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/packages/pointer_interceptor/example/ios/Flutter/AppFrameworkInfo.plist b/packages/pointer_interceptor/example/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..6b4c0f7
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>App</string>
+ <key>CFBundleIdentifier</key>
+ <string>io.flutter.flutter.app</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>App</string>
+ <key>CFBundlePackageType</key>
+ <string>FMWK</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>MinimumOSVersion</key>
+ <string>8.0</string>
+</dict>
+</plist>
diff --git a/packages/pointer_interceptor/example/ios/Flutter/Debug.xcconfig b/packages/pointer_interceptor/example/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Flutter/Debug.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/packages/pointer_interceptor/example/ios/Flutter/Release.xcconfig b/packages/pointer_interceptor/example/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Flutter/Release.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.pbxproj b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..c6759a6
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,471 @@
+// !$*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 */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
+ 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 = "<group>"; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "<group>";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ );
+ sourceTree = "<group>";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ );
+ path = Runner;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1020;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 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 = "<group>";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+/* 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;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 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;
+ 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;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "group:Runner.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IDEDidComputeMac32BitWarning</key>
+ <true/>
+</dict>
+</plist>
diff --git a/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>PreviewsEnabled</key>
+ <false/>
+</dict>
+</plist>
diff --git a/packages/pointer_interceptor/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..a28140c
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "1020"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Profile"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/packages/pointer_interceptor/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/pointer_interceptor/example/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "group:Runner.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/pointer_interceptor/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pointer_interceptor/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IDEDidComputeMac32BitWarning</key>
+ <true/>
+</dict>
+</plist>
diff --git a/packages/pointer_interceptor/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/pointer_interceptor/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>PreviewsEnabled</key>
+ <false/>
+</dict>
+</plist>
diff --git a/packages/pointer_interceptor/example/ios/Runner/AppDelegate.swift b/packages/pointer_interceptor/example/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..70693e4
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+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/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..dc9ada4
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..28c6bf0
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..f091b6b
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cde121
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..d0ef06e
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..dcdc230
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..c8f9ed8
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..75b2d16
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..c4df70d
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..6a84f41
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..d0e1f58
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/packages/pointer_interceptor/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/pointer_interceptor/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
+ <viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
+ </imageView>
+ </subviews>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ <constraints>
+ <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
+ <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
+ </constraints>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+ <resources>
+ <image name="LaunchImage" width="168" height="185"/>
+ </resources>
+</document>
diff --git a/packages/pointer_interceptor/example/ios/Runner/Base.lproj/Main.storyboard b/packages/pointer_interceptor/example/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ </dependencies>
+ <scenes>
+ <!--Flutter View Controller-->
+ <scene sceneID="tne-QT-ifu">
+ <objects>
+ <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+ <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+ </objects>
+ </scene>
+ </scenes>
+</document>
diff --git a/packages/pointer_interceptor/example/ios/Runner/Info.plist b/packages/pointer_interceptor/example/ios/Runner/Info.plist
new file mode 100644
index 0000000..a060db6
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>example</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>$(FLUTTER_BUILD_NAME)</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>$(FLUTTER_BUILD_NUMBER)</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UIViewControllerBasedStatusBarAppearance</key>
+ <false/>
+</dict>
+</plist>
diff --git a/packages/pointer_interceptor/example/ios/Runner/Runner-Bridging-Header.h b/packages/pointer_interceptor/example/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/packages/pointer_interceptor/example/lib/main.dart b/packages/pointer_interceptor/example/lib/main.dart
new file mode 100644
index 0000000..619380c
--- /dev/null
+++ b/packages/pointer_interceptor/example/lib/main.dart
@@ -0,0 +1,210 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore: avoid_web_libraries_in_flutter
+import 'dart:html' as html;
+
+import 'package:flutter/material.dart';
+import 'package:pointer_interceptor/pointer_interceptor.dart';
+
+import 'src/shim/dart_ui.dart' as ui;
+
+const String _htmlElementViewType = '_htmlElementViewType';
+const double _videoWidth = 640;
+const double _videoHeight = 480;
+
+/// The html.Element that will be rendered underneath the flutter UI.
+html.Element htmlElement = html.DivElement()
+ ..style.width = '100%'
+ ..style.height = '100%'
+ ..style.backgroundColor = '#fabada'
+ ..style.cursor = 'auto'
+ ..id = 'background-html-view';
+
+// See other examples commented out below...
+
+// html.Element htmlElement = html.VideoElement()
+// ..style.width = '100%'
+// ..style.height = '100%'
+// ..style.cursor = 'auto'
+// ..style.backgroundColor = 'black'
+// ..id = 'background-html-view'
+// ..src = 'https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4'
+// ..poster = 'https://peach.blender.org/wp-content/uploads/title_anouncement.jpg?x11217'
+// ..controls = true;
+
+// html.Element htmlElement = html.IFrameElement()
+// ..width = '100%'
+// ..height = '100%'
+// ..id = 'background-html-view'
+// ..src = 'https://www.youtube.com/embed/IyFZznAk69U'
+// ..style.border = 'none';
+
+void main() {
+ runApp(const MyApp());
+}
+
+/// Main app
+class MyApp extends StatelessWidget {
+ /// Creates main app.
+ const MyApp({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ ui.platformViewRegistry.registerViewFactory(_htmlElementViewType,
+ (int viewId) {
+ final html.Element wrapper = html.DivElement();
+ wrapper.append(htmlElement);
+ return wrapper;
+ });
+
+ return const MaterialApp(
+ title: 'Stopping Clicks with some DOM',
+ home: MyHomePage(),
+ );
+ }
+}
+
+/// First page
+class MyHomePage extends StatefulWidget {
+ /// Creates first page.
+ const MyHomePage({Key? key}) : super(key: key);
+
+ @override
+ _MyHomePageState createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+ String _lastClick = 'none';
+
+ void _clickedOn(String key) {
+ setState(() {
+ _lastClick = key;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('PointerInterceptor demo'),
+ actions: <Widget>[
+ PointerInterceptor(
+ // debug: true,
+ child: IconButton(
+ icon: const Icon(Icons.add_alert),
+ tooltip: 'AppBar Icon',
+ onPressed: () {
+ _clickedOn('appbar-icon');
+ },
+ ),
+ ),
+ ],
+ ),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ Text(
+ 'Last click on: $_lastClick',
+ key: const Key('last-clicked'),
+ ),
+ Container(
+ color: Colors.black,
+ width: _videoWidth,
+ height: _videoHeight,
+ child: Stack(
+ alignment: Alignment.center,
+ children: <Widget>[
+ HtmlElement(
+ onClick: () {
+ _clickedOn('html-element');
+ },
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: <Widget>[
+ ElevatedButton(
+ key: const Key('transparent-button'),
+ child: const Text('Never calls onPressed'),
+ onPressed: () {
+ _clickedOn('transparent-button');
+ },
+ ),
+ PointerInterceptor(
+ child: ElevatedButton(
+ key: const Key('clickable-button'),
+ child: const Text('Works As Expected'),
+ onPressed: () {
+ _clickedOn('clickable-button');
+ },
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ floatingActionButton: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: <Widget>[
+ PointerInterceptor(
+ // debug: true,
+ child: FloatingActionButton(
+ child: const Icon(Icons.navigation),
+ onPressed: () {
+ _clickedOn('fab-1');
+ },
+ ),
+ ),
+ ],
+ ),
+ drawer: Drawer(
+ child: PointerInterceptor(
+ // debug: true, // Enable this to "see" the interceptor covering the column.
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ ListTile(
+ title: const Text('Item 1'),
+ onTap: () {
+ _clickedOn('drawer-item-1');
+ },
+ ),
+ ListTile(
+ title: const Text('Item 2'),
+ onTap: () {
+ _clickedOn('drawer-item-2');
+ },
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+/// Initialize the videoPlayer, then render the corresponding view...
+class HtmlElement extends StatelessWidget {
+ /// Constructor
+ const HtmlElement({Key? key, required this.onClick}) : super(key: key);
+
+ /// A function to run when the element is clicked
+ final Function onClick;
+
+ @override
+ Widget build(BuildContext context) {
+ htmlElement.onClick.listen((_) {
+ onClick();
+ });
+
+ return const HtmlElementView(
+ viewType: _htmlElementViewType,
+ );
+ }
+}
diff --git a/packages/pointer_interceptor/example/lib/src/shim/dart_ui.dart b/packages/pointer_interceptor/example/lib/src/shim/dart_ui.dart
new file mode 100644
index 0000000..442115e
--- /dev/null
+++ b/packages/pointer_interceptor/example/lib/src/shim/dart_ui.dart
@@ -0,0 +1,10 @@
+// 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.
+
+/// This file shims dart:ui in web-only scenarios, getting rid of the need to
+/// suppress analyzer warnings.
+
+// TODO(dit): 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/pointer_interceptor/example/lib/src/shim/dart_ui_fake.dart b/packages/pointer_interceptor/example/lib/src/shim/dart_ui_fake.dart
new file mode 100644
index 0000000..787e349
--- /dev/null
+++ b/packages/pointer_interceptor/example/lib/src/shim/dart_ui_fake.dart
@@ -0,0 +1,30 @@
+// 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.
+
+// 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
+// ignore: camel_case_types
+class platformViewRegistry {
+ /// Shim for registerViewFactory
+ /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72
+ static void registerViewFactory(
+ String viewTypeId, dynamic 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
+// ignore: camel_case_types
+class webOnlyAssetManager {
+ /// Shim for getAssetUrl.
+ /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L45
+ static String getAssetUrl(String asset) {
+ return '';
+ }
+}
+
+/// Signature of callbacks that have no arguments and return no data.
+typedef VoidCallback = void Function();
diff --git a/packages/pointer_interceptor/example/lib/src/shim/dart_ui_real.dart b/packages/pointer_interceptor/example/lib/src/shim/dart_ui_real.dart
new file mode 100644
index 0000000..90ecd7d
--- /dev/null
+++ b/packages/pointer_interceptor/example/lib/src/shim/dart_ui_real.dart
@@ -0,0 +1,5 @@
+// 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.
+
+export 'dart:ui';
diff --git a/packages/pointer_interceptor/example/pubspec.yaml b/packages/pointer_interceptor/example/pubspec.yaml
new file mode 100644
index 0000000..2ed4117
--- /dev/null
+++ b/packages/pointer_interceptor/example/pubspec.yaml
@@ -0,0 +1,30 @@
+name: pointer_interceptor_example
+description: An example app for the pointer_interceptor package.
+publish_to: 'none'
+version: 1.0.0
+
+environment:
+ sdk: ">=2.12.0-0 <3.0.0"
+ flutter: ">=1.26.0-0" # For integration_test from sdk
+
+dependencies:
+ flutter:
+ sdk: flutter
+ pointer_interceptor:
+ path: ../
+
+dev_dependencies:
+ flutter_driver:
+ sdk: flutter
+ flutter_test:
+ sdk: flutter
+ integration_test:
+ sdk: flutter
+
+# The following section is specific to Flutter.
+flutter:
+
+ # The following line ensures that the Material Icons font is
+ # included with your application, so that you can use the icons in
+ # the material Icons class.
+ uses-material-design: true
diff --git a/packages/pointer_interceptor/example/test_driver/integration_test.dart b/packages/pointer_interceptor/example/test_driver/integration_test.dart
new file mode 100644
index 0000000..64e2248
--- /dev/null
+++ b/packages/pointer_interceptor/example/test_driver/integration_test.dart
@@ -0,0 +1,7 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:integration_test/integration_test_driver.dart';
+
+Future<void> main() async => integrationDriver();
diff --git a/packages/pointer_interceptor/example/web/favicon.png b/packages/pointer_interceptor/example/web/favicon.png
new file mode 100644
index 0000000..8aaa46a
--- /dev/null
+++ b/packages/pointer_interceptor/example/web/favicon.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/web/icons/Icon-192.png b/packages/pointer_interceptor/example/web/icons/Icon-192.png
new file mode 100644
index 0000000..b749bfe
--- /dev/null
+++ b/packages/pointer_interceptor/example/web/icons/Icon-192.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/web/icons/Icon-512.png b/packages/pointer_interceptor/example/web/icons/Icon-512.png
new file mode 100644
index 0000000..88cfd48
--- /dev/null
+++ b/packages/pointer_interceptor/example/web/icons/Icon-512.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/web/index.html b/packages/pointer_interceptor/example/web/index.html
new file mode 100644
index 0000000..1460b5e
--- /dev/null
+++ b/packages/pointer_interceptor/example/web/index.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <!--
+ If you are serving your web app in a path other than the root, change the
+ href value below to reflect the base path you are serving from.
+
+ The path provided below has to start and end with a slash "/" in order for
+ it to work correctly.
+
+ Fore more details:
+ * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
+ -->
+ <base href="/">
+
+ <meta charset="UTF-8">
+ <meta content="IE=Edge" http-equiv="X-UA-Compatible">
+ <meta name="description" content="A new Flutter project.">
+
+ <!-- iOS meta tags & icons -->
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="apple-mobile-web-app-status-bar-style" content="black">
+ <meta name="apple-mobile-web-app-title" content="example">
+ <link rel="apple-touch-icon" href="icons/Icon-192.png">
+
+ <!-- Favicon -->
+ <link rel="icon" type="image/png" href="favicon.png"/>
+
+ <title>example</title>
+ <link rel="manifest" href="manifest.json">
+</head>
+<body>
+ <!-- This script installs service_worker.js to provide PWA functionality to
+ application. For more information, see:
+ https://developers.google.com/web/fundamentals/primers/service-workers -->
+ <script>
+ if ('serviceWorker' in navigator) {
+ window.addEventListener('flutter-first-frame', function () {
+ navigator.serviceWorker.register('flutter_service_worker.js');
+ });
+ }
+ </script>
+ <script src="main.dart.js" type="application/javascript"></script>
+</body>
+</html>
diff --git a/packages/pointer_interceptor/example/web/manifest.json b/packages/pointer_interceptor/example/web/manifest.json
new file mode 100644
index 0000000..8c01291
--- /dev/null
+++ b/packages/pointer_interceptor/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/pointer_interceptor/lib/pointer_interceptor.dart b/packages/pointer_interceptor/lib/pointer_interceptor.dart
new file mode 100644
index 0000000..c760403
--- /dev/null
+++ b/packages/pointer_interceptor/lib/pointer_interceptor.dart
@@ -0,0 +1,7 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+library pointer_interceptor;
+
+export 'src/mobile.dart' if (dart.library.html) 'src/web.dart';
diff --git a/packages/pointer_interceptor/lib/src/mobile.dart b/packages/pointer_interceptor/lib/src/mobile.dart
new file mode 100644
index 0000000..1f561ac
--- /dev/null
+++ b/packages/pointer_interceptor/lib/src/mobile.dart
@@ -0,0 +1,29 @@
+// 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/widgets.dart';
+
+/// A [Widget] that prevents clicks from being swallowed by [HtmlElementView]s.
+class PointerInterceptor extends StatelessWidget {
+ /// Create a `PointerInterceptor` wrapping a `child`.
+ const PointerInterceptor({
+ required this.child,
+ this.debug = false,
+ Key? key,
+ }) : super(key: key);
+
+ /// The `Widget` that is being wrapped by this `PointerInterceptor`.
+ final Widget child;
+
+ /// When true, the widget renders with a semi-transparent red background, for debug purposes.
+ ///
+ /// This is useful when rendering this as a "layout" widget, like the root child
+ /// of a `Drawer`.
+ final bool debug;
+
+ @override
+ Widget build(BuildContext context) {
+ return child;
+ }
+}
diff --git a/packages/pointer_interceptor/lib/src/shim/dart_ui.dart b/packages/pointer_interceptor/lib/src/shim/dart_ui.dart
new file mode 100644
index 0000000..442115e
--- /dev/null
+++ b/packages/pointer_interceptor/lib/src/shim/dart_ui.dart
@@ -0,0 +1,10 @@
+// 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.
+
+/// This file shims dart:ui in web-only scenarios, getting rid of the need to
+/// suppress analyzer warnings.
+
+// TODO(dit): 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/pointer_interceptor/lib/src/shim/dart_ui_fake.dart b/packages/pointer_interceptor/lib/src/shim/dart_ui_fake.dart
new file mode 100644
index 0000000..787e349
--- /dev/null
+++ b/packages/pointer_interceptor/lib/src/shim/dart_ui_fake.dart
@@ -0,0 +1,30 @@
+// 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.
+
+// 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
+// ignore: camel_case_types
+class platformViewRegistry {
+ /// Shim for registerViewFactory
+ /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72
+ static void registerViewFactory(
+ String viewTypeId, dynamic 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
+// ignore: camel_case_types
+class webOnlyAssetManager {
+ /// Shim for getAssetUrl.
+ /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L45
+ static String getAssetUrl(String asset) {
+ return '';
+ }
+}
+
+/// Signature of callbacks that have no arguments and return no data.
+typedef VoidCallback = void Function();
diff --git a/packages/pointer_interceptor/lib/src/shim/dart_ui_real.dart b/packages/pointer_interceptor/lib/src/shim/dart_ui_real.dart
new file mode 100644
index 0000000..90ecd7d
--- /dev/null
+++ b/packages/pointer_interceptor/lib/src/shim/dart_ui_real.dart
@@ -0,0 +1,5 @@
+// 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.
+
+export 'dart:ui';
diff --git a/packages/pointer_interceptor/lib/src/web.dart b/packages/pointer_interceptor/lib/src/web.dart
new file mode 100644
index 0000000..f59c4a3
--- /dev/null
+++ b/packages/pointer_interceptor/lib/src/web.dart
@@ -0,0 +1,89 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore: avoid_web_libraries_in_flutter
+import 'dart:html' as html;
+
+import 'package:flutter/widgets.dart';
+
+import 'shim/dart_ui.dart' as ui;
+
+const String _viewType = '__webPointerInterceptorViewType__';
+const String _debug = 'debug__';
+
+// Computes a "view type" for different configurations of the widget.
+String _getViewType({bool debug = false}) {
+ return debug ? _viewType + _debug : _viewType;
+}
+
+// Registers a viewFactory for this widget.
+void _registerFactory({bool debug = false}) {
+ final String viewType = _getViewType(debug: debug);
+ ui.platformViewRegistry.registerViewFactory(viewType, (int viewId) {
+ final html.Element htmlElement = html.DivElement()
+ ..style.top = '0'
+ ..style.right = '0'
+ ..style.bottom = '0'
+ ..style.left = '0'
+ ..style.position = 'relative';
+ if (debug) {
+ htmlElement.style.backgroundColor = 'rgba(255, 0, 0, .5)';
+ }
+ return htmlElement;
+ });
+}
+
+/// The web implementation of the `PointerInterceptor` widget.
+///
+/// A `Widget` that prevents clicks from being swallowed by [HtmlElementView]s.
+class PointerInterceptor extends StatelessWidget {
+ /// Creates a PointerInterceptor for the web.
+ PointerInterceptor({
+ required this.child,
+ this.debug = false,
+ Key? key,
+ }) : super(key: key) {
+ if (!_registered) {
+ _register();
+ }
+ }
+
+ /// The `Widget` that is being wrapped by this `PointerInterceptor`.
+ final Widget child;
+
+ /// When true, the widget renders with a semi-transparent red background, for debug purposes.
+ ///
+ /// This is useful when rendering this as a "layout" widget, like the root child
+ /// of a [Drawer].
+ final bool debug;
+
+ // Keeps track if this widget has already registered its view factories or not.
+ static bool _registered = false;
+
+ // Registers the view factories for the interceptor widgets.
+ static void _register() {
+ assert(!_registered);
+
+ _registerFactory();
+ _registerFactory(debug: true);
+
+ _registered = true;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final String viewType = _getViewType(debug: debug);
+ return Stack(
+ alignment: Alignment.center,
+ children: <Widget>[
+ Positioned.fill(
+ child: HtmlElementView(
+ viewType: viewType,
+ ),
+ ),
+ child,
+ ],
+ );
+ }
+}
diff --git a/packages/pointer_interceptor/pubspec.yaml b/packages/pointer_interceptor/pubspec.yaml
new file mode 100644
index 0000000..5bea65f
--- /dev/null
+++ b/packages/pointer_interceptor/pubspec.yaml
@@ -0,0 +1,16 @@
+name: pointer_interceptor
+description: A widget to prevent clicks from being swallowed by underlying HtmlElementViews on the web.
+repository: https://github.com/flutter/packages
+version: 0.9.0
+
+environment:
+ sdk: ">=2.12.0-0 <3.0.0"
+ flutter: ">=1.17.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
diff --git a/packages/pointer_interceptor/test/README.md b/packages/pointer_interceptor/test/README.md
new file mode 100644
index 0000000..7c5b4ad
--- /dev/null
+++ b/packages/pointer_interceptor/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/pointer_interceptor/test/tests_exist_elsewhere_test.dart b/packages/pointer_interceptor/test/tests_exist_elsewhere_test.dart
new file mode 100644
index 0000000..334f521
--- /dev/null
+++ b/packages/pointer_interceptor/test/tests_exist_elsewhere_test.dart
@@ -0,0 +1,10 @@
+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/web_benchmarks/CHANGELOG.md b/packages/web_benchmarks/CHANGELOG.md
new file mode 100644
index 0000000..e243d25
--- /dev/null
+++ b/packages/web_benchmarks/CHANGELOG.md
@@ -0,0 +1,19 @@
+## 0.0.5
+
+* Updated dependencies to allow broader versions for upstream packages.
+
+## 0.0.4
+
+* Updated dependencies to allow broader versions for upstream packages.
+
+## 0.0.3
+
+* Fixed benchmarks failing due to trace format change for begin frame.
+
+## 0.0.2
+
+* Improve console messages.
+
+## 0.0.1 - Initial release.
+
+* Provide a benchmark server (host-side) and a benchmark client (browser-side).
diff --git a/packages/web_benchmarks/LICENSE b/packages/web_benchmarks/LICENSE
new file mode 100644
index 0000000..4611350
--- /dev/null
+++ b/packages/web_benchmarks/LICENSE
@@ -0,0 +1,27 @@
+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/web_benchmarks/README.md b/packages/web_benchmarks/README.md
new file mode 100644
index 0000000..ba6a1cb
--- /dev/null
+++ b/packages/web_benchmarks/README.md
@@ -0,0 +1,15 @@
+# web_benchmarks
+
+A benchmark harness for Flutter Web apps. Currently only supports running
+benchmarks in Chrome.
+
+# Writing a benchmark
+
+An example benchmark can be found in [test/web_benchmark_test.dart][1].
+
+A web benchmark is made of two parts: a client and a server. The client is code
+that runs in the browser together with the benchmark code. The server serves the
+app's code and assets. Additionally, the server communicates with the browser to
+extract the performance traces.
+
+[1]: https://github.com/flutter/packages/blob/master/packages/web_benchmarks/test/web_benchmarks_test.dart
diff --git a/packages/web_benchmarks/lib/client.dart b/packages/web_benchmarks/lib/client.dart
new file mode 100644
index 0000000..bb1fc76
--- /dev/null
+++ b/packages/web_benchmarks/lib/client.dart
@@ -0,0 +1,383 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert' show json;
+import 'dart:html' as html;
+import 'dart:math' as math;
+
+import 'package:meta/meta.dart';
+
+import 'src/common.dart';
+import 'src/recorder.dart';
+export 'src/recorder.dart';
+
+/// Signature for a function that creates a [Recorder].
+typedef RecorderFactory = Recorder Function();
+
+/// List of all benchmarks that run in the devicelab.
+///
+/// When adding a new benchmark, add it to this map. Make sure that the name
+/// of your benchmark is unique.
+Map<String, RecorderFactory> _benchmarks;
+
+final LocalBenchmarkServerClient _client = LocalBenchmarkServerClient();
+
+/// Starts a local benchmark client to run [benchmarks].
+///
+/// Usually used in combination with a benchmark server, which orders the
+/// client to run each benchmark in order.
+///
+/// When used without a server, prompts the user to select a benchmark to
+/// run next.
+Future<void> runBenchmarks(Map<String, RecorderFactory> benchmarks) async {
+ assert(benchmarks != null);
+
+ // Set local benchmarks.
+ _benchmarks = benchmarks;
+
+ // Check if the benchmark server wants us to run a specific benchmark.
+ final String nextBenchmark = await _client.requestNextBenchmark();
+
+ if (nextBenchmark == LocalBenchmarkServerClient.kManualFallback) {
+ _fallbackToManual(
+ 'The server did not tell us which benchmark to run next.');
+ return;
+ }
+
+ await _runBenchmark(nextBenchmark);
+ html.window.location.reload();
+}
+
+Future<void> _runBenchmark(String benchmarkName) async {
+ final RecorderFactory recorderFactory = _benchmarks[benchmarkName];
+
+ if (recorderFactory == null) {
+ _fallbackToManual('Benchmark $benchmarkName not found.');
+ return;
+ }
+
+ await runZoned<Future<void>>(
+ () async {
+ final Recorder recorder = recorderFactory();
+ final Runner runner = recorder.isTracingEnabled && !_client.isInManualMode
+ ? Runner(
+ recorder: recorder,
+ setUpAllDidRun: () =>
+ _client.startPerformanceTracing(benchmarkName),
+ tearDownAllWillRun: _client.stopPerformanceTracing,
+ )
+ : Runner(recorder: recorder);
+
+ final Profile profile = await runner.run();
+ if (!_client.isInManualMode) {
+ await _client.sendProfileData(profile);
+ } else {
+ _printResultsToScreen(profile);
+ print(profile);
+ }
+ },
+ zoneSpecification: ZoneSpecification(
+ print: (Zone self, ZoneDelegate parent, Zone zone, String line) async {
+ if (_client.isInManualMode) {
+ parent.print(zone, '[$benchmarkName] $line');
+ } else {
+ await _client.printToConsole(line);
+ }
+ },
+ handleUncaughtError: (
+ Zone self,
+ ZoneDelegate parent,
+ Zone zone,
+ Object error,
+ StackTrace stackTrace,
+ ) async {
+ if (_client.isInManualMode) {
+ parent.print(zone, '[$benchmarkName] $error, $stackTrace');
+ parent.handleUncaughtError(zone, error, stackTrace);
+ } else {
+ await _client.reportError(error, stackTrace);
+ }
+ },
+ ),
+ );
+}
+
+void _fallbackToManual(String error) {
+ html.document.body.appendHtml('''
+ <div id="manual-panel">
+ <h3>$error</h3>
+
+ <p>Choose one of the following benchmarks:</p>
+
+ <!-- Absolutely position it so it receives the clicks and not the glasspane -->
+ <ul style="position: absolute">
+ ${_benchmarks.keys.map((String name) => '<li><button id="$name">$name</button></li>').join('\n')}
+ </ul>
+ </div>
+ ''',
+ validator: html.NodeValidatorBuilder()
+ ..allowHtml5()
+ ..allowInlineStyles());
+
+ for (final String benchmarkName in _benchmarks.keys) {
+ final html.Element button = html.document.querySelector('#$benchmarkName');
+ button.addEventListener('click', (_) {
+ final html.Element manualPanel =
+ html.document.querySelector('#manual-panel');
+ manualPanel?.remove();
+ _runBenchmark(benchmarkName);
+ });
+ }
+}
+
+/// Visualizes results on the Web page for manual inspection.
+void _printResultsToScreen(Profile profile) {
+ html.document.body.innerHtml = '<h2>${profile.name}</h2>';
+
+ profile.scoreData.forEach((String scoreKey, Timeseries timeseries) {
+ html.document.body.appendHtml('<h2>$scoreKey</h2>');
+ html.document.body.appendHtml('<pre>${timeseries.computeStats()}</pre>');
+ html.document.body.append(TimeseriesVisualization(timeseries).render());
+ });
+}
+
+/// Draws timeseries data and statistics on a canvas.
+class TimeseriesVisualization {
+ /// Creates a visualization for a [Timeseries].
+ TimeseriesVisualization(this._timeseries) {
+ _stats = _timeseries.computeStats();
+ _canvas = html.CanvasElement();
+ _screenWidth = html.window.screen.width;
+ _canvas.width = _screenWidth;
+ _canvas.height = (_kCanvasHeight * html.window.devicePixelRatio).round();
+ _canvas.style
+ ..width = '100%'
+ ..height = '${_kCanvasHeight}px'
+ ..outline = '1px solid green';
+ _ctx = _canvas.context2D;
+
+ // The amount of vertical space available on the chart. Because some
+ // outliers can be huge they can dwarf all the useful values. So we
+ // limit it to 1.5 x the biggest non-outlier.
+ _maxValueChartRange = 1.5 *
+ _stats.samples
+ .where((AnnotatedSample sample) => !sample.isOutlier)
+ .map<double>((AnnotatedSample sample) => sample.magnitude)
+ .fold<double>(0, math.max);
+ }
+
+ static const double _kCanvasHeight = 200;
+
+ final Timeseries _timeseries;
+ TimeseriesStats _stats;
+ html.CanvasElement _canvas;
+ html.CanvasRenderingContext2D _ctx;
+ int _screenWidth;
+
+ // Used to normalize benchmark values to chart height.
+ double _maxValueChartRange;
+
+ /// Converts a sample value to vertical canvas coordinates.
+ ///
+ /// This does not work for horizontal coordinates.
+ double _normalized(double value) {
+ return _kCanvasHeight * value / _maxValueChartRange;
+ }
+
+ /// A utility for drawing lines.
+ void drawLine(num x1, num y1, num x2, num y2) {
+ _ctx.beginPath();
+ _ctx.moveTo(x1, y1);
+ _ctx.lineTo(x2, y2);
+ _ctx.stroke();
+ }
+
+ /// Renders the timeseries into a `<canvas>` and returns the canvas element.
+ html.CanvasElement render() {
+ _ctx.translate(0, _kCanvasHeight * html.window.devicePixelRatio);
+ _ctx.scale(1, -html.window.devicePixelRatio);
+
+ final double barWidth = _screenWidth / _stats.samples.length;
+ double xOffset = 0;
+ for (int i = 0; i < _stats.samples.length; i++) {
+ final AnnotatedSample sample = _stats.samples[i];
+
+ if (sample.isWarmUpValue) {
+ // Put gray background behing warm-up samples.
+ _ctx.fillStyle = 'rgba(200,200,200,1)';
+ _ctx.fillRect(xOffset, 0, barWidth, _normalized(_maxValueChartRange));
+ }
+
+ if (sample.magnitude > _maxValueChartRange) {
+ // The sample value is so big it doesn't fit on the chart. Paint it purple.
+ _ctx.fillStyle = 'rgba(100,50,100,0.8)';
+ } else if (sample.isOutlier) {
+ // The sample is an outlier, color it light red.
+ _ctx.fillStyle = 'rgba(255,50,50,0.6)';
+ } else {
+ // A non-outlier sample, color it light blue.
+ _ctx.fillStyle = 'rgba(50,50,255,0.6)';
+ }
+
+ _ctx.fillRect(xOffset, 0, barWidth - 1, _normalized(sample.magnitude));
+ xOffset += barWidth;
+ }
+
+ // Draw a horizontal solid line corresponding to the average.
+ _ctx.lineWidth = 1;
+ drawLine(0, _normalized(_stats.average), _screenWidth,
+ _normalized(_stats.average));
+
+ // Draw a horizontal dashed line corresponding to the outlier cut off.
+ _ctx.setLineDash(<num>[5, 5]);
+ drawLine(0, _normalized(_stats.outlierCutOff), _screenWidth,
+ _normalized(_stats.outlierCutOff));
+
+ // Draw a light red band that shows the noise (1 stddev in each direction).
+ _ctx.fillStyle = 'rgba(255,50,50,0.3)';
+ _ctx.fillRect(
+ 0,
+ _normalized(_stats.average * (1 - _stats.noise)),
+ _screenWidth,
+ _normalized(2 * _stats.average * _stats.noise),
+ );
+
+ return _canvas;
+ }
+}
+
+/// Implements the client REST API for the local benchmark server.
+///
+/// The local server is optional. If it is not available the benchmark UI must
+/// implement a manual fallback. This allows debugging benchmarks using plain
+/// `flutter run`.
+class LocalBenchmarkServerClient {
+ /// This value is returned by [requestNextBenchmark].
+ static const String kManualFallback = '__manual_fallback__';
+
+ /// Whether we fell back to manual mode.
+ ///
+ /// This happens when you run benchmarks using plain `flutter run` rather than
+ /// devicelab test harness. The test harness spins up a special server that
+ /// provides API for automatically picking the next benchmark to run.
+ bool isInManualMode;
+
+ /// Asks the local server for the name of the next benchmark to run.
+ ///
+ /// Returns [kManualFallback] if local server is not available (uses 404 as a
+ /// signal).
+ Future<String> requestNextBenchmark() async {
+ final html.HttpRequest request = await _requestXhr(
+ '/next-benchmark',
+ method: 'POST',
+ mimeType: 'application/json',
+ sendData: json.encode(_benchmarks.keys.toList()),
+ );
+
+ // `kEndOfBenchmarks` is expected when the benchmark server is telling us there are no more benchmarks to run.
+ // 404 is expected when the benchmark is run using plain `flutter run`, which does not provide "next-benchmark" handler.
+ if (request.responseText == kEndOfBenchmarks || request.status == 404) {
+ isInManualMode = true;
+ return kManualFallback;
+ }
+
+ isInManualMode = false;
+ return request.responseText;
+ }
+
+ void _checkNotManualMode() {
+ if (isInManualMode) {
+ throw StateError('Operation not supported in manual fallback mode.');
+ }
+ }
+
+ /// Asks the local server to begin tracing performance.
+ ///
+ /// This uses the chrome://tracing tracer, which is not available from within
+ /// the page itself, and therefore must be controlled from outside using the
+ /// DevTools Protocol.
+ Future<void> startPerformanceTracing(String benchmarkName) async {
+ _checkNotManualMode();
+ await html.HttpRequest.request(
+ '/start-performance-tracing?label=$benchmarkName',
+ method: 'POST',
+ mimeType: 'application/json',
+ );
+ }
+
+ /// Stops the performance tracing session started by [startPerformanceTracing].
+ Future<void> stopPerformanceTracing() async {
+ _checkNotManualMode();
+ await html.HttpRequest.request(
+ '/stop-performance-tracing',
+ method: 'POST',
+ mimeType: 'application/json',
+ );
+ }
+
+ /// Sends the profile data collected by the benchmark to the local benchmark
+ /// server.
+ Future<void> sendProfileData(Profile profile) async {
+ _checkNotManualMode();
+ final html.HttpRequest request = await html.HttpRequest.request(
+ '/profile-data',
+ method: 'POST',
+ mimeType: 'application/json',
+ sendData: json.encode(profile.toJson()),
+ );
+ if (request.status != 200) {
+ throw Exception('Failed to report profile data to benchmark server. '
+ 'The server responded with status code ${request.status}.');
+ }
+ }
+
+ /// Reports an error to the benchmark server.
+ ///
+ /// The server will halt the devicelab task and log the error.
+ Future<void> reportError(dynamic error, StackTrace stackTrace) async {
+ _checkNotManualMode();
+ await html.HttpRequest.request(
+ '/on-error',
+ method: 'POST',
+ mimeType: 'application/json',
+ sendData: json.encode(<String, dynamic>{
+ 'error': '$error',
+ 'stackTrace': '$stackTrace',
+ }),
+ );
+ }
+
+ /// Reports a message about the demo to the benchmark server.
+ Future<void> printToConsole(String report) async {
+ _checkNotManualMode();
+ await html.HttpRequest.request(
+ '/print-to-console',
+ method: 'POST',
+ mimeType: 'text/plain',
+ sendData: report,
+ );
+ }
+
+ /// This is the same as calling [html.HttpRequest.request] but it doesn't
+ /// crash on 404, which we use to detect `flutter run`.
+ Future<html.HttpRequest> _requestXhr(
+ String url, {
+ @required String method,
+ @required String mimeType,
+ @required dynamic sendData,
+ }) {
+ final Completer<html.HttpRequest> completer = Completer<html.HttpRequest>();
+ final html.HttpRequest xhr = html.HttpRequest();
+ method ??= 'GET';
+ xhr.open(method, url, async: true);
+ xhr.overrideMimeType(mimeType);
+ xhr.onLoad.listen((html.ProgressEvent e) {
+ completer.complete(xhr);
+ });
+ xhr.onError.listen(completer.completeError);
+ xhr.send(sendData);
+ return completer.future;
+ }
+}
diff --git a/packages/web_benchmarks/lib/server.dart b/packages/web_benchmarks/lib/server.dart
new file mode 100644
index 0000000..77300c0
--- /dev/null
+++ b/packages/web_benchmarks/lib/server.dart
@@ -0,0 +1,62 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io' as io;
+
+import 'package:logging/logging.dart';
+import 'package:meta/meta.dart';
+
+import 'src/benchmark_result.dart';
+import 'src/runner.dart';
+
+export 'src/benchmark_result.dart';
+
+/// The default port number used by the local benchmark server.
+const int defaultBenchmarkServerPort = 9999;
+
+/// The default port number used for Chrome DevTool Protocol.
+const int defaultChromeDebugPort = 10000;
+
+/// Builds and serves a Flutter Web app, collects raw benchmark data and
+/// summarizes the result as a [BenchmarkResult].
+///
+/// [benchmarkAppDirectory] is the directory containing the app that's being
+/// benchmarked. The app is expected to use `package:web_benchmarks/client.dart`
+/// and call the `runBenchmarks` function to run the benchmarks.
+///
+/// [entryPoint] is the path to the main app file that runs the benchmark. It
+/// can be different (and typically is) from the production entry point of the
+/// app.
+///
+/// If [useCanvasKit] is true, builds the app in CanvasKit mode.
+///
+/// [benchmarkServerPort] is the port this benchmark server serves the app on.
+/// By default uses [defaultBenchmarkServerPort].
+///
+/// [chromeDebugPort] is the port Chrome uses for DevTool Protocol used to
+/// extract tracing data. By default uses [defaultChromeDebugPort].
+///
+/// If [headless] is true, runs Chrome without UI. In particular, this is
+/// useful in environments (e.g. CI) that doesn't have a display.
+Future<BenchmarkResults> serveWebBenchmark({
+ @required io.Directory benchmarkAppDirectory,
+ @required String entryPoint,
+ @required bool useCanvasKit,
+ int benchmarkServerPort = defaultBenchmarkServerPort,
+ int chromeDebugPort = defaultChromeDebugPort,
+ bool headless = true,
+}) async {
+ // Reduce logging level. Otherwise, package:webkit_inspection_protocol is way too spammy.
+ Logger.root.level = Level.INFO;
+
+ return BenchmarkServer(
+ benchmarkAppDirectory: benchmarkAppDirectory,
+ entryPoint: entryPoint,
+ useCanvasKit: useCanvasKit,
+ benchmarkServerPort: benchmarkServerPort,
+ chromeDebugPort: chromeDebugPort,
+ headless: headless,
+ ).run();
+}
diff --git a/packages/web_benchmarks/lib/src/benchmark_result.dart b/packages/web_benchmarks/lib/src/benchmark_result.dart
new file mode 100644
index 0000000..4f6c458
--- /dev/null
+++ b/packages/web_benchmarks/lib/src/benchmark_result.dart
@@ -0,0 +1,59 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:meta/meta.dart';
+
+/// A single benchmark score value collected from the benchmark.
+class BenchmarkScore {
+ /// Creates a benchmark score.
+ ///
+ /// [metric] and [value] must not be null.
+ BenchmarkScore({
+ @required this.metric,
+ @required this.value,
+ }) : assert(metric != null && value != null);
+
+ /// The name of the metric that this score is categorized under.
+ ///
+ /// Scores collected over time under the same name can be visualized as a
+ /// timeline.
+ final String metric;
+
+ /// The result of measuring a particular metric in this benchmark run.
+ final num value;
+
+ /// Serializes the benchmark metric to a JSON object.
+ Map<String, dynamic> toJson() {
+ return <String, dynamic>{
+ 'metric': metric,
+ 'value': value,
+ };
+ }
+}
+
+/// The result of running a benchmark.
+class BenchmarkResults {
+ /// Constructs a result containing scores from a single run benchmark run.
+ BenchmarkResults(this.scores);
+
+ /// Scores collected in a benchmark run.
+ final Map<String, List<BenchmarkScore>> scores;
+
+ /// Serializes benchmark metrics to JSON.
+ Map<String, List<Map<String, dynamic>>> toJson() {
+ return scores.map<String, List<Map<String, dynamic>>>(
+ (String benchmarkName, List<BenchmarkScore> scores) {
+ return MapEntry<String, List<Map<String, dynamic>>>(
+ benchmarkName,
+ scores
+ .map<Map<String, dynamic>>(
+ (BenchmarkScore score) => <String, dynamic>{
+ 'metric': score.metric,
+ 'value': score.value,
+ })
+ .toList(),
+ );
+ });
+ }
+}
diff --git a/packages/web_benchmarks/lib/src/browser.dart b/packages/web_benchmarks/lib/src/browser.dart
new file mode 100644
index 0000000..413adfe
--- /dev/null
+++ b/packages/web_benchmarks/lib/src/browser.dart
@@ -0,0 +1,612 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert' show json, utf8, LineSplitter, JsonEncoder;
+import 'dart:io' as io;
+import 'dart:math' as math;
+
+import 'package:path/path.dart' as path;
+import 'package:meta/meta.dart';
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
+
+import 'common.dart';
+
+/// Options passed to Chrome when launching it.
+class ChromeOptions {
+ /// Creates chrome options.
+ ///
+ /// [windowWidth], [windowHeight], and [headless] must not be null.
+ ChromeOptions({
+ this.userDataDirectory,
+ this.url,
+ this.windowWidth = 1024,
+ this.windowHeight = 1024,
+ this.headless,
+ this.debugPort,
+ });
+
+ /// If not null passed as `--user-data-dir`.
+ final String userDataDirectory;
+
+ /// If not null launches a Chrome tab at this URL.
+ final String url;
+
+ /// The width of the Chrome window.
+ ///
+ /// This is important for screenshots and benchmarks.
+ final int windowWidth;
+
+ /// The height of the Chrome window.
+ ///
+ /// This is important for screenshots and benchmarks.
+ final int windowHeight;
+
+ /// Launches code in "headless" mode, which allows running Chrome in
+ /// environments without a display, such as LUCI and Cirrus.
+ final bool headless;
+
+ /// The port Chrome will use for its debugging protocol.
+ ///
+ /// If null, Chrome is launched without debugging. When running in headless
+ /// mode without a debug port, Chrome quits immediately. For most tests it is
+ /// typical to set [headless] to true and set a non-null debug port.
+ final int debugPort;
+}
+
+/// A function called when the Chrome process encounters an error.
+typedef ChromeErrorCallback = void Function(String);
+
+/// Manages a single Chrome process.
+class Chrome {
+ Chrome._(this._chromeProcess, this._onError, this._debugConnection,
+ bool headless) {
+ if (headless) {
+ // In headless mode, if the Chrome process quits before it was asked to
+ // quit, notify the error listener. If it's not running headless, the
+ // developer may close the browser any time, so it's not considered to
+ // be an error.
+ _chromeProcess.exitCode.then((int exitCode) {
+ if (!_isStopped) {
+ _onError(
+ 'Chrome process exited prematurely with exit code $exitCode');
+ }
+ });
+ }
+ }
+
+ /// Launches Chrome with the give [options].
+ ///
+ /// The [onError] callback is called with an error message when the Chrome
+ /// process encounters an error. In particular, [onError] is called when the
+ /// Chrome process exits prematurely, i.e. before [stop] is called.
+ static Future<Chrome> launch(ChromeOptions options,
+ {String workingDirectory, @required ChromeErrorCallback onError}) async {
+ if (!io.Platform.isWindows) {
+ final io.ProcessResult versionResult = io.Process.runSync(
+ _findSystemChromeExecutable(), const <String>['--version']);
+ print('Launching ${versionResult.stdout}');
+ } else {
+ print('Launching Chrome...');
+ }
+
+ final bool withDebugging = options.debugPort != null;
+ final List<String> args = <String>[
+ if (options.userDataDirectory != null)
+ '--user-data-dir=${options.userDataDirectory}',
+ if (options.url != null) options.url,
+ if (io.Platform.environment['CHROME_NO_SANDBOX'] == 'true')
+ '--no-sandbox',
+ if (options.headless) '--headless',
+ if (withDebugging) '--remote-debugging-port=${options.debugPort}',
+ '--window-size=${options.windowWidth},${options.windowHeight}',
+ '--disable-extensions',
+ '--disable-popup-blocking',
+ // Indicates that the browser is in "browse without sign-in" (Guest session) mode.
+ '--bwsi',
+ '--no-first-run',
+ '--no-default-browser-check',
+ '--disable-default-apps',
+ '--disable-translate',
+ ];
+ final io.Process chromeProcess = await io.Process.start(
+ _findSystemChromeExecutable(),
+ args,
+ workingDirectory: workingDirectory,
+ );
+
+ WipConnection debugConnection;
+ if (withDebugging) {
+ debugConnection =
+ await _connectToChromeDebugPort(chromeProcess, options.debugPort);
+ }
+
+ return Chrome._(chromeProcess, onError, debugConnection, options.headless);
+ }
+
+ final io.Process _chromeProcess;
+ final ChromeErrorCallback _onError;
+ final WipConnection _debugConnection;
+ bool _isStopped = false;
+
+ Completer<void> _tracingCompleter;
+ StreamSubscription<WipEvent> _tracingSubscription;
+ List<Map<String, dynamic>> _tracingData;
+
+ /// Starts recording a performance trace.
+ ///
+ /// If there is already a tracing session in progress, throws an error. Call
+ /// [endRecordingPerformance] before starting a new tracing session.
+ ///
+ /// The [label] is for debugging convenience.
+ Future<void> beginRecordingPerformance(String label) async {
+ if (_tracingCompleter != null) {
+ throw StateError(
+ 'Cannot start a new performance trace. A tracing session labeled '
+ '"$label" is already in progress.');
+ }
+ _tracingCompleter = Completer<void>();
+ _tracingData = <Map<String, dynamic>>[];
+
+ // Subscribe to tracing events prior to calling "Tracing.start". Otherwise,
+ // we'll miss tracing data.
+ _tracingSubscription =
+ _debugConnection.onNotification.listen((WipEvent event) {
+ // We receive data as a sequence of "Tracing.dataCollected" followed by
+ // "Tracing.tracingComplete" at the end. Until "Tracing.tracingComplete"
+ // is received, the data may be incomplete.
+ if (event.method == 'Tracing.tracingComplete') {
+ _tracingCompleter.complete();
+ _tracingSubscription.cancel();
+ _tracingSubscription = null;
+ } else if (event.method == 'Tracing.dataCollected') {
+ final dynamic value = event.params['value'];
+ if (value is! List) {
+ throw FormatException(
+ '"Tracing.dataCollected" returned malformed data. '
+ 'Expected a List but got: ${value.runtimeType}');
+ }
+ _tracingData.addAll(event.params['value'].cast<Map<String, dynamic>>());
+ }
+ });
+ await _debugConnection.sendCommand('Tracing.start', <String, dynamic>{
+ // The choice of categories is as follows:
+ //
+ // blink:
+ // provides everything on the UI thread, including scripting,
+ // style recalculations, layout, painting, and some compositor
+ // work.
+ // blink.user_timing:
+ // provides marks recorded using window.performance. We use marks
+ // to find frames that the benchmark cares to measure.
+ // gpu:
+ // provides tracing data from the GPU data
+ // disabled due to https://bugs.chromium.org/p/chromium/issues/detail?id=1068259
+ // TODO(yjbanov): extract useful GPU data
+ 'categories': 'blink,blink.user_timing',
+ 'transferMode': 'SendAsStream',
+ });
+ }
+
+ /// Stops a performance tracing session started by [beginRecordingPerformance].
+ ///
+ /// Returns all the collected tracing data unfiltered.
+ Future<List<Map<String, dynamic>>> endRecordingPerformance() async {
+ await _debugConnection.sendCommand('Tracing.end');
+ await _tracingCompleter.future;
+ final List<Map<String, dynamic>> data = _tracingData;
+ _tracingCompleter = null;
+ _tracingData = null;
+ return data;
+ }
+
+ /// Stops the Chrome process.
+ void stop() {
+ _isStopped = true;
+ _chromeProcess.kill();
+ }
+
+ /// Resolves when the Chrome process exits.
+ Future<void> get whenExits async {
+ await _chromeProcess.exitCode;
+ }
+}
+
+String _findSystemChromeExecutable() {
+ // On some environments, such as the Dart HHH tester, Chrome resides in a
+ // non-standard location and is provided via the following environment
+ // variable.
+ final String envExecutable = io.Platform.environment['CHROME_EXECUTABLE'];
+ if (envExecutable != null) {
+ return envExecutable;
+ }
+
+ if (io.Platform.isLinux) {
+ final io.ProcessResult which =
+ io.Process.runSync('which', <String>['google-chrome']);
+
+ if (which.exitCode != 0) {
+ throw Exception('Failed to locate system Chrome installation.');
+ }
+
+ final String output = which.stdout;
+ return output.trim();
+ } else if (io.Platform.isMacOS) {
+ return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
+ } else if (io.Platform.isWindows) {
+ const String kWindowsExecutable = r'Google\Chrome\Application\chrome.exe';
+ final List<String> kWindowsPrefixes = <String>[
+ io.Platform.environment['LOCALAPPDATA'],
+ io.Platform.environment['PROGRAMFILES'],
+ io.Platform.environment['PROGRAMFILES(X86)'],
+ ];
+ final String windowsPrefix = kWindowsPrefixes.firstWhere((String prefix) {
+ if (prefix == null) {
+ return false;
+ }
+ final String expectedPath = path.join(prefix, kWindowsExecutable);
+ return io.File(expectedPath).existsSync();
+ }, orElse: () => '.');
+ return path.join(windowsPrefix, kWindowsExecutable);
+ } else {
+ throw Exception(
+ 'Web benchmarks cannot run on ${io.Platform.operatingSystem}.');
+ }
+}
+
+/// Waits for Chrome to print DevTools URI and connects to it.
+Future<WipConnection> _connectToChromeDebugPort(
+ io.Process chromeProcess, int port) async {
+ chromeProcess.stdout
+ .transform(utf8.decoder)
+ .transform(const LineSplitter())
+ .listen((String line) {
+ print('[CHROME]: $line');
+ });
+
+ await chromeProcess.stderr
+ .transform(utf8.decoder)
+ .transform(const LineSplitter())
+ .map((String line) {
+ print('[CHROME]: $line');
+ return line;
+ }).firstWhere((String line) => line.startsWith('DevTools listening'),
+ orElse: () {
+ throw Exception('Expected Chrome to print "DevTools listening" string '
+ 'with DevTools URL, but the string was never printed.');
+ });
+
+ final Uri devtoolsUri =
+ await _getRemoteDebuggerUrl(Uri.parse('http://localhost:$port'));
+ print('Connecting to DevTools: $devtoolsUri');
+ final ChromeConnection chromeConnection = ChromeConnection('localhost', port);
+ final Iterable<ChromeTab> tabs =
+ (await chromeConnection.getTabs()).where((ChromeTab tab) {
+ return tab.url.startsWith('http://localhost');
+ });
+ final ChromeTab tab = tabs.single;
+ final WipConnection debugConnection = await tab.connect();
+ print('Connected to Chrome tab: ${tab.title} (${tab.url})');
+ return debugConnection;
+}
+
+/// Gets the Chrome debugger URL for the web page being benchmarked.
+Future<Uri> _getRemoteDebuggerUrl(Uri base) async {
+ final io.HttpClient client = io.HttpClient();
+ final io.HttpClientRequest request =
+ await client.getUrl(base.resolve('/json/list'));
+ final io.HttpClientResponse response = await request.close();
+ final List<dynamic> jsonObject =
+ await json.fuse(utf8).decoder.bind(response).single;
+ if (jsonObject == null || jsonObject.isEmpty) {
+ return base;
+ }
+ return base.resolve(jsonObject.first['webSocketDebuggerUrl']);
+}
+
+/// Summarizes a Blink trace down to a few interesting values.
+class BlinkTraceSummary {
+ BlinkTraceSummary._({
+ @required this.averageBeginFrameTime,
+ @required this.averageUpdateLifecyclePhasesTime,
+ }) : averageTotalUIFrameTime =
+ averageBeginFrameTime + averageUpdateLifecyclePhasesTime;
+
+ /// Summarizes Blink trace from the raw JSON trace.
+ static BlinkTraceSummary fromJson(List<Map<String, dynamic>> traceJson) {
+ try {
+ // Convert raw JSON data to BlinkTraceEvent objects sorted by timestamp.
+ List<BlinkTraceEvent> events = traceJson
+ .map<BlinkTraceEvent>(BlinkTraceEvent.fromJson)
+ .toList()
+ ..sort((BlinkTraceEvent a, BlinkTraceEvent b) => a.ts - b.ts);
+
+ Exception noMeasuredFramesFound() => Exception(
+ 'No measured frames found in benchmark tracing data. This likely '
+ 'indicates a bug in the benchmark. For example, the benchmark failed '
+ 'to pump enough frames. It may also indicate a change in Chrome\'s '
+ 'tracing data format. Check if Chrome version changed recently and '
+ 'adjust the parsing code accordingly.',
+ );
+
+ // Use the pid from the first "measured_frame" event since the event is
+ // emitted by the script running on the process we're interested in.
+ //
+ // We previously tried using the "CrRendererMain" event. However, for
+ // reasons unknown, Chrome in the devicelab refuses to emit this event
+ // sometimes, causing to flakes.
+ final BlinkTraceEvent firstMeasuredFrameEvent = events.firstWhere(
+ (BlinkTraceEvent event) => event.isBeginMeasuredFrame,
+ orElse: () => throw noMeasuredFramesFound(),
+ );
+
+ if (firstMeasuredFrameEvent == null) {
+ // This happens in benchmarks that do not measure frames, such as some
+ // of the text layout benchmarks.
+ return null;
+ }
+
+ final int tabPid = firstMeasuredFrameEvent.pid;
+
+ // Filter out data from unrelated processes
+ events = events
+ .where((BlinkTraceEvent element) => element.pid == tabPid)
+ .toList();
+
+ // Extract frame data.
+ final List<BlinkFrame> frames = <BlinkFrame>[];
+ int skipCount = 0;
+ BlinkFrame frame = BlinkFrame();
+ for (final BlinkTraceEvent event in events) {
+ if (event.isBeginFrame) {
+ frame.beginFrame = event;
+ } else if (event.isUpdateAllLifecyclePhases) {
+ frame.updateAllLifecyclePhases = event;
+ if (frame.endMeasuredFrame != null) {
+ frames.add(frame);
+ } else {
+ skipCount += 1;
+ }
+ frame = BlinkFrame();
+ } else if (event.isBeginMeasuredFrame) {
+ frame.beginMeasuredFrame = event;
+ } else if (event.isEndMeasuredFrame) {
+ frame.endMeasuredFrame = event;
+ }
+ }
+
+ print('Extracted ${frames.length} measured frames.');
+ print('Skipped $skipCount non-measured frames.');
+
+ if (frames.isEmpty) {
+ throw noMeasuredFramesFound();
+ }
+
+ // Compute averages and summarize.
+ return BlinkTraceSummary._(
+ averageBeginFrameTime: _computeAverageDuration(
+ frames.map((BlinkFrame frame) => frame.beginFrame).toList()),
+ averageUpdateLifecyclePhasesTime: _computeAverageDuration(frames
+ .map((BlinkFrame frame) => frame.updateAllLifecyclePhases)
+ .toList()),
+ );
+ } catch (_, __) {
+ final io.File traceFile = io.File('./chrome-trace.json');
+ io.stderr.writeln(
+ 'Failed to interpret the Chrome trace contents. The trace was saved in ${traceFile.path}');
+ traceFile.writeAsStringSync(
+ const JsonEncoder.withIndent(' ').convert(traceJson));
+ rethrow;
+ }
+ }
+
+ /// The average duration of "WebViewImpl::beginFrame" events.
+ ///
+ /// This event contains all of scripting time of an animation frame, plus an
+ /// unknown small amount of work browser does before and after scripting.
+ final Duration averageBeginFrameTime;
+
+ /// The average duration of "WebViewImpl::updateAllLifecyclePhases" events.
+ ///
+ /// This event contains style, layout, painting, and compositor computations,
+ /// which are not included in the scripting time. This event does not
+ /// include GPU time, which happens on a separate thread.
+ final Duration averageUpdateLifecyclePhasesTime;
+
+ /// The average sum of [averageBeginFrameTime] and
+ /// [averageUpdateLifecyclePhasesTime].
+ ///
+ /// This value contains the vast majority of work the UI thread performs in
+ /// any given animation frame.
+ final Duration averageTotalUIFrameTime;
+
+ @override
+ String toString() => '$BlinkTraceSummary('
+ 'averageBeginFrameTime: ${averageBeginFrameTime.inMicroseconds / 1000}ms, '
+ 'averageUpdateLifecyclePhasesTime: ${averageUpdateLifecyclePhasesTime.inMicroseconds / 1000}ms)';
+}
+
+/// Contains events pertaining to a single frame in the Blink trace data.
+class BlinkFrame {
+ /// Corresponds to 'WebViewImpl::beginFrame' event.
+ BlinkTraceEvent beginFrame;
+
+ /// Corresponds to 'WebViewImpl::updateAllLifecyclePhases' event.
+ BlinkTraceEvent updateAllLifecyclePhases;
+
+ /// Corresponds to 'measured_frame' begin event.
+ BlinkTraceEvent beginMeasuredFrame;
+
+ /// Corresponds to 'measured_frame' end event.
+ BlinkTraceEvent endMeasuredFrame;
+}
+
+/// Takes a list of events that have non-null [BlinkTraceEvent.tdur] computes
+/// their average as a [Duration] value.
+Duration _computeAverageDuration(List<BlinkTraceEvent> events) {
+ // Compute the sum of "tdur" fields of the last kMeasuredSampleCount events.
+ final double sum = events
+ .skip(math.max(events.length - kMeasuredSampleCount, 0))
+ .fold(0.0, (double previousValue, BlinkTraceEvent event) {
+ if (event.tdur == null) {
+ throw FormatException('Trace event lacks "tdur" field: $event');
+ }
+ return previousValue + event.tdur;
+ });
+ final int sampleCount = math.min(events.length, kMeasuredSampleCount);
+ return Duration(microseconds: sum ~/ sampleCount);
+}
+
+/// An event collected by the Blink tracer (in Chrome accessible using chrome://tracing).
+///
+/// See also:
+/// * https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
+class BlinkTraceEvent {
+ BlinkTraceEvent._({
+ @required this.args,
+ @required this.cat,
+ @required this.name,
+ @required this.ph,
+ @required this.pid,
+ @required this.tid,
+ @required this.ts,
+ @required this.tts,
+ @required this.tdur,
+ });
+
+ /// Parses an event from its JSON representation.
+ ///
+ /// Sample event encoded as JSON (the data is bogus, this just shows the format):
+ ///
+ /// ```
+ /// {
+ /// "name": "myName",
+ /// "cat": "category,list",
+ /// "ph": "B",
+ /// "ts": 12345,
+ /// "pid": 123,
+ /// "tid": 456,
+ /// "args": {
+ /// "someArg": 1,
+ /// "anotherArg": {
+ /// "value": "my value"
+ /// }
+ /// }
+ /// }
+ /// ```
+ ///
+ /// For detailed documentation of the format see:
+ ///
+ /// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
+ static BlinkTraceEvent fromJson(Map<String, dynamic> json) {
+ return BlinkTraceEvent._(
+ args: json['args'],
+ cat: json['cat'],
+ name: json['name'],
+ ph: json['ph'],
+ pid: _readInt(json, 'pid'),
+ tid: _readInt(json, 'tid'),
+ ts: _readInt(json, 'ts'),
+ tts: _readInt(json, 'tts'),
+ tdur: _readInt(json, 'tdur'),
+ );
+ }
+
+ /// Event-specific data.
+ final Map<String, dynamic> args;
+
+ /// Event category.
+ final String cat;
+
+ /// Event name.
+ final String name;
+
+ /// Event "phase".
+ final String ph;
+
+ /// Process ID of the process that emitted the event.
+ final int pid;
+
+ /// Thread ID of the thread that emitted the event.
+ final int tid;
+
+ /// Timestamp in microseconds using tracer clock.
+ final int ts;
+
+ /// Timestamp in microseconds using thread clock.
+ final int tts;
+
+ /// Event duration in microseconds.
+ final int tdur;
+
+ /// A "begin frame" event contains all of the scripting time of an animation
+ /// frame (JavaScript, WebAssembly), plus a negligible amount of internal
+ /// browser overhead.
+ ///
+ /// This event does not include non-UI thread scripting, such as web workers,
+ /// service workers, and CSS Paint paintlets.
+ ///
+ /// WebViewImpl::beginFrame was used in earlier versions of Chrome, kept
+ /// for compatibility.
+ ///
+ /// This event is a duration event that has its `tdur` populated.
+ bool get isBeginFrame =>
+ ph == 'X' &&
+ (name == 'WebViewImpl::beginFrame' ||
+ name == 'WebFrameWidgetBase::BeginMainFrame');
+
+ /// An "update all lifecycle phases" event contains UI thread computations
+ /// related to an animation frame that's outside the scripting phase.
+ ///
+ /// This event includes style recalculation, layer tree update, layout,
+ /// painting, and parts of compositing work.
+ ///
+ /// This event is a duration event that has its `tdur` populated.
+ bool get isUpdateAllLifecyclePhases =>
+ ph == 'X' && name == 'WebViewImpl::updateAllLifecyclePhases';
+
+ /// Whether this is the beginning of a "measured_frame" event.
+ ///
+ /// This event is a custom event emitted by our benchmark test harness.
+ ///
+ /// See also:
+ /// * `recorder.dart`, which emits this event.
+ bool get isBeginMeasuredFrame => ph == 'b' && name == 'measured_frame';
+
+ /// Whether this is the end of a "measured_frame" event.
+ ///
+ /// This event is a custom event emitted by our benchmark test harness.
+ ///
+ /// See also:
+ /// * `recorder.dart`, which emits this event.
+ bool get isEndMeasuredFrame => ph == 'e' && name == 'measured_frame';
+
+ @override
+ String toString() => '$BlinkTraceEvent('
+ 'args: ${json.encode(args)}, '
+ 'cat: $cat, '
+ 'name: $name, '
+ 'ph: $ph, '
+ 'pid: $pid, '
+ 'tid: $tid, '
+ 'ts: $ts, '
+ 'tts: $tts, '
+ 'tdur: $tdur)';
+}
+
+/// Read an integer out of [json] stored under [key].
+///
+/// Since JSON does not distinguish between `int` and `double`, extra
+/// validation and conversion is needed.
+///
+/// Returns null if the value is null.
+int _readInt(Map<String, dynamic> json, String key) {
+ final num jsonValue = json[key];
+
+ if (jsonValue == null) {
+ return null;
+ }
+
+ return jsonValue.toInt();
+}
diff --git a/packages/web_benchmarks/lib/src/common.dart b/packages/web_benchmarks/lib/src/common.dart
new file mode 100644
index 0000000..7795287
--- /dev/null
+++ b/packages/web_benchmarks/lib/src/common.dart
@@ -0,0 +1,16 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// This library contains code that's common between the client and the server.
+///
+/// The code must be compilable both as a command-line program and as a web
+/// program.
+library web_benchmarks.common;
+
+/// The number of samples we use to collect statistics from.
+const int kMeasuredSampleCount = 100;
+
+/// A special value returned by the `/next-benchmark` HTTP POST request when
+/// all benchmarks have run and there are no more benchmarks to run.
+const String kEndOfBenchmarks = '__end_of_benchmarks__';
diff --git a/packages/web_benchmarks/lib/src/recorder.dart b/packages/web_benchmarks/lib/src/recorder.dart
new file mode 100644
index 0000000..f9e6df5
--- /dev/null
+++ b/packages/web_benchmarks/lib/src/recorder.dart
@@ -0,0 +1,1243 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:html' as html;
+import 'dart:js_util' as js_util;
+import 'dart:math' as math;
+import 'dart:ui';
+
+import 'package:meta/meta.dart';
+
+import 'package:flutter/gestures.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/widgets.dart';
+
+import 'common.dart';
+
+/// The number of samples from warm-up iterations.
+///
+/// We warm-up the benchmark prior to measuring to allow JIT and caches to settle.
+const int _kWarmUpSampleCount = 200;
+
+/// The total number of samples collected by a benchmark.
+const int kTotalSampleCount = _kWarmUpSampleCount + kMeasuredSampleCount;
+
+/// A benchmark metric that includes frame-related computations prior to
+/// submitting layer and picture operations to the underlying renderer, such as
+/// HTML and CanvasKit. During this phase we compute transforms, clips, and
+/// other information needed for rendering.
+const String kProfilePrerollFrame = 'preroll_frame';
+
+/// A benchmark metric that includes submitting layer and picture information
+/// to the renderer.
+const String kProfileApplyFrame = 'apply_frame';
+
+/// Measures the amount of time [action] takes.
+Duration timeAction(VoidCallback action) {
+ final Stopwatch stopwatch = Stopwatch()..start();
+ action();
+ stopwatch.stop();
+ return stopwatch.elapsed;
+}
+
+/// A function that performs asynchronous work.
+typedef AsyncVoidCallback = Future<void> Function();
+
+/// An [AsyncVoidCallback] that doesn't do anything.
+///
+/// This is used just so we don't have to deal with null all over the place.
+Future<void> _dummyAsyncVoidCallback() async {}
+
+/// Runs the benchmark using the given [recorder].
+///
+/// Notifies about "set up" and "tear down" events via the [setUpAllDidRun]
+/// and [tearDownAllWillRun] callbacks.
+@sealed
+class Runner {
+ /// Creates a runner for the [recorder].
+ ///
+ /// All arguments must not be null.
+ Runner({
+ @required this.recorder,
+ this.setUpAllDidRun = _dummyAsyncVoidCallback,
+ this.tearDownAllWillRun = _dummyAsyncVoidCallback,
+ });
+
+ /// The recorder that will run and record the benchmark.
+ final Recorder recorder;
+
+ /// Called immediately after [Recorder.setUpAll] future is resolved.
+ ///
+ /// This is useful, for example, to kick off a profiler or a tracer such that
+ /// the "set up" computations are not included in the metrics.
+ final AsyncVoidCallback setUpAllDidRun;
+
+ /// Called just before calling [Recorder.tearDownAll].
+ ///
+ /// This is useful, for example, to stop a profiler or a tracer such that
+ /// the "tear down" computations are not included in the metrics.
+ final AsyncVoidCallback tearDownAllWillRun;
+
+ /// Runs the benchmark and reports the results.
+ Future<Profile> run() async {
+ await recorder.setUpAll();
+ await setUpAllDidRun();
+ final Profile profile = await recorder.run();
+ await tearDownAllWillRun();
+ await recorder.tearDownAll();
+ return profile;
+ }
+}
+
+/// Base class for benchmark recorders.
+///
+/// Each benchmark recorder has a [name] and a [run] method at a minimum.
+abstract class Recorder {
+ Recorder._(this.name, this.isTracingEnabled);
+
+ /// Whether this recorder requires tracing using Chrome's DevTools Protocol's
+ /// "Tracing" API.
+ final bool isTracingEnabled;
+
+ /// The name of the benchmark.
+ ///
+ /// The results displayed in the Flutter Dashboard will use this name as a
+ /// prefix.
+ final String name;
+
+ /// Returns the recorded profile.
+ ///
+ /// This value is only available while the benchmark is running.
+ Profile get profile;
+
+ /// Whether the benchmark should continue running.
+ ///
+ /// Returns `false` if the benchmark collected enough data and it's time to
+ /// stop.
+ bool shouldContinue() => profile.shouldContinue();
+
+ /// Called once before all runs of this benchmark recorder.
+ ///
+ /// This is useful for doing one-time setup work that's needed for the
+ /// benchmark.
+ Future<void> setUpAll() async {}
+
+ /// The implementation of the benchmark that will produce a [Profile].
+ Future<Profile> run();
+
+ /// Called once after all runs of this benchmark recorder.
+ ///
+ /// This is useful for doing one-time clean up work after the benchmark is
+ /// complete.
+ Future<void> tearDownAll() async {}
+}
+
+/// A recorder for benchmarking raw execution of Dart code.
+///
+/// This is useful for benchmarks that don't need frames or widgets.
+///
+/// Example:
+///
+/// ```
+/// class BenchForLoop extends RawRecorder {
+/// BenchForLoop() : super(name: benchmarkName);
+///
+/// static const String benchmarkName = 'for_loop';
+///
+/// @override
+/// void body(Profile profile) {
+/// profile.record('loop', () {
+/// double x = 0;
+/// for (int i = 0; i < 10000000; i++) {
+/// x *= 1.5;
+/// }
+/// });
+/// }
+/// }
+/// ```
+abstract class RawRecorder extends Recorder {
+ /// Creates a raw benchmark recorder with a name.
+ ///
+ /// [name] must not be null.
+ RawRecorder({@required String name}) : super._(name, false);
+
+ /// The body of the benchmark.
+ ///
+ /// This is the part that records measurements of the benchmark.
+ void body(Profile profile);
+
+ @override
+ Profile get profile => _profile;
+ Profile _profile;
+
+ @override
+ @nonVirtual
+ Future<Profile> run() async {
+ _profile = Profile(name: name);
+ do {
+ await Future<void>.delayed(Duration.zero);
+ body(_profile);
+ } while (shouldContinue());
+ return _profile;
+ }
+}
+
+/// A recorder for benchmarking interactions with the engine without the
+/// framework by directly exercising [SceneBuilder].
+///
+/// To implement a benchmark, extend this class and implement [onDrawFrame].
+///
+/// Example:
+///
+/// ```
+/// class BenchDrawCircle extends SceneBuilderRecorder {
+/// BenchDrawCircle() : super(name: benchmarkName);
+///
+/// static const String benchmarkName = 'draw_circle';
+///
+/// @override
+/// void onDrawFrame(SceneBuilder sceneBuilder) {
+/// final PictureRecorder pictureRecorder = PictureRecorder();
+/// final Canvas canvas = Canvas(pictureRecorder);
+/// final Paint paint = Paint()..color = const Color.fromARGB(255, 255, 0, 0);
+/// final Size windowSize = window.physicalSize;
+/// canvas.drawCircle(windowSize.center(Offset.zero), 50.0, paint);
+/// final Picture picture = pictureRecorder.endRecording();
+/// sceneBuilder.addPicture(picture);
+/// }
+/// }
+/// ```
+abstract class SceneBuilderRecorder extends Recorder {
+ /// Creates a [SceneBuilder] benchmark recorder.
+ ///
+ /// [name] must not be null.
+ SceneBuilderRecorder({@required String name}) : super._(name, true);
+
+ @override
+ Profile get profile => _profile;
+ Profile _profile;
+
+ /// Called from [Window.onBeginFrame].
+ @mustCallSuper
+ void onBeginFrame() {}
+
+ /// Called on every frame.
+ ///
+ /// An implementation should exercise the [sceneBuilder] to build a frame.
+ /// However, it must not call [SceneBuilder.build] or [Window.render].
+ /// Instead the benchmark harness will call them and time them appropriately.
+ void onDrawFrame(SceneBuilder sceneBuilder);
+
+ @override
+ Future<Profile> run() {
+ final Completer<Profile> profileCompleter = Completer<Profile>();
+ _profile = Profile(name: name);
+
+ window.onBeginFrame = (_) {
+ try {
+ startMeasureFrame(profile);
+ onBeginFrame();
+ } catch (error, stackTrace) {
+ profileCompleter.completeError(error, stackTrace);
+ rethrow;
+ }
+ };
+ window.onDrawFrame = () {
+ try {
+ _profile.record('drawFrameDuration', () {
+ final SceneBuilder sceneBuilder = SceneBuilder();
+ onDrawFrame(sceneBuilder);
+ _profile.record('sceneBuildDuration', () {
+ final Scene scene = sceneBuilder.build();
+ _profile.record('windowRenderDuration', () {
+ window.render(scene);
+ }, reported: false);
+ }, reported: false);
+ }, reported: true);
+ endMeasureFrame();
+
+ if (shouldContinue()) {
+ window.scheduleFrame();
+ } else {
+ profileCompleter.complete(_profile);
+ }
+ } catch (error, stackTrace) {
+ profileCompleter.completeError(error, stackTrace);
+ rethrow;
+ }
+ };
+ window.scheduleFrame();
+ return profileCompleter.future;
+ }
+}
+
+/// A recorder for benchmarking interactions with the framework by creating
+/// widgets.
+///
+/// To implement a benchmark, extend this class and implement [createWidget].
+///
+/// Example:
+///
+/// ```
+/// class BenchListView extends WidgetRecorder {
+/// BenchListView() : super(name: benchmarkName);
+///
+/// static const String benchmarkName = 'bench_list_view';
+///
+/// @override
+/// Widget createWidget() {
+/// return Directionality(
+/// textDirection: TextDirection.ltr,
+/// child: _TestListViewWidget(),
+/// );
+/// }
+/// }
+///
+/// class _TestListViewWidget extends StatefulWidget {
+/// @override
+/// State<StatefulWidget> createState() {
+/// return _TestListViewWidgetState();
+/// }
+/// }
+///
+/// class _TestListViewWidgetState extends State<_TestListViewWidget> {
+/// ScrollController scrollController;
+///
+/// @override
+/// void initState() {
+/// super.initState();
+/// scrollController = ScrollController();
+/// Timer.run(() async {
+/// bool forward = true;
+/// while (true) {
+/// await scrollController.animateTo(
+/// forward ? 300 : 0,
+/// curve: Curves.linear,
+/// duration: const Duration(seconds: 1),
+/// );
+/// forward = !forward;
+/// }
+/// });
+/// }
+///
+/// @override
+/// Widget build(BuildContext context) {
+/// return ListView.builder(
+/// controller: scrollController,
+/// itemCount: 10000,
+/// itemBuilder: (BuildContext context, int index) {
+/// return Text('Item #$index');
+/// },
+/// );
+/// }
+/// }
+/// ```
+abstract class WidgetRecorder extends Recorder implements FrameRecorder {
+ /// Creates a widget benchmark recorder.
+ ///
+ /// [name] must not be null.
+ ///
+ /// If [useCustomWarmUp] is true, delegates the benchmark warm-up to the
+ /// benchmark implementation instead of using a built-in strategy. The
+ /// benchmark is expected to call [Profile.stopWarmingUp] to signal that
+ /// the warm-up phase is finished.
+ WidgetRecorder({
+ @required String name,
+ this.useCustomWarmUp = false,
+ }) : super._(name, true);
+
+ /// Creates a widget to be benchmarked.
+ ///
+ /// The widget must create its own animation to drive the benchmark. The
+ /// animation should continue indefinitely. The benchmark harness will stop
+ /// pumping frames automatically.
+ Widget createWidget();
+
+ final List<VoidCallback> _didStopCallbacks = <VoidCallback>[];
+ @override
+ void registerDidStop(VoidCallback fn) {
+ _didStopCallbacks.add(fn);
+ }
+
+ @override
+ Profile profile;
+ Completer<void> _runCompleter;
+
+ /// Whether to delimit warm-up frames in a custom way.
+ final bool useCustomWarmUp;
+
+ Stopwatch _drawFrameStopwatch;
+
+ @override
+ @mustCallSuper
+ void frameWillDraw() {
+ startMeasureFrame(profile);
+ _drawFrameStopwatch = Stopwatch()..start();
+ }
+
+ @override
+ @mustCallSuper
+ void frameDidDraw() {
+ endMeasureFrame();
+ profile.addDataPoint('drawFrameDuration', _drawFrameStopwatch.elapsed,
+ reported: true);
+
+ if (shouldContinue()) {
+ window.scheduleFrame();
+ } else {
+ for (final VoidCallback fn in _didStopCallbacks) {
+ fn();
+ }
+ _runCompleter.complete();
+ }
+ }
+
+ @override
+ void _onError(dynamic error, StackTrace stackTrace) {
+ _runCompleter.completeError(error, stackTrace);
+ }
+
+ @override
+ Future<Profile> run() async {
+ _runCompleter = Completer<void>();
+ final Profile localProfile =
+ profile = Profile(name: name, useCustomWarmUp: useCustomWarmUp);
+ final _RecordingWidgetsBinding binding =
+ _RecordingWidgetsBinding.ensureInitialized();
+ final Widget widget = createWidget();
+
+ registerEngineBenchmarkValueListener(kProfilePrerollFrame, (num value) {
+ localProfile.addDataPoint(
+ kProfilePrerollFrame,
+ Duration(microseconds: value.toInt()),
+ reported: false,
+ );
+ });
+ registerEngineBenchmarkValueListener(kProfileApplyFrame, (num value) {
+ localProfile.addDataPoint(
+ kProfileApplyFrame,
+ Duration(microseconds: value.toInt()),
+ reported: false,
+ );
+ });
+
+ binding._beginRecording(this, widget);
+
+ try {
+ await _runCompleter.future;
+ return localProfile;
+ } finally {
+ stopListeningToEngineBenchmarkValues(kProfilePrerollFrame);
+ stopListeningToEngineBenchmarkValues(kProfileApplyFrame);
+ _runCompleter = null;
+ profile = null;
+ }
+ }
+}
+
+/// A recorder for measuring the performance of building a widget from scratch
+/// starting from an empty frame.
+///
+/// The recorder will call [createWidget] and render it, then it will pump
+/// another frame that clears the screen. It repeats this process, measuring the
+/// performance of frames that render the widget and ignoring the frames that
+/// clear the screen.
+abstract class WidgetBuildRecorder extends Recorder implements FrameRecorder {
+ /// Creates a widget build benchmark recorder.
+ ///
+ /// [name] must not be null.
+ WidgetBuildRecorder({@required String name}) : super._(name, true);
+
+ /// Creates a widget to be benchmarked.
+ ///
+ /// The widget is not expected to animate as we only care about construction
+ /// of the widget. If you are interested in benchmarking an animation,
+ /// consider using [WidgetRecorder].
+ Widget createWidget();
+
+ final List<VoidCallback> _didStopCallbacks = <VoidCallback>[];
+ @override
+ void registerDidStop(VoidCallback fn) {
+ _didStopCallbacks.add(fn);
+ }
+
+ @override
+ Profile profile;
+ Completer<void> _runCompleter;
+
+ Stopwatch _drawFrameStopwatch;
+
+ /// Whether in this frame we should call [createWidget] and render it.
+ ///
+ /// If false, then this frame will clear the screen.
+ bool showWidget = true;
+
+ /// The state that hosts the widget under test.
+ _WidgetBuildRecorderHostState _hostState;
+
+ Widget _getWidgetForFrame() {
+ if (showWidget) {
+ return createWidget();
+ } else {
+ return null;
+ }
+ }
+
+ @override
+ @mustCallSuper
+ void frameWillDraw() {
+ if (showWidget) {
+ startMeasureFrame(profile);
+ _drawFrameStopwatch = Stopwatch()..start();
+ }
+ }
+
+ @override
+ @mustCallSuper
+ void frameDidDraw() {
+ // Only record frames that show the widget.
+ if (showWidget) {
+ endMeasureFrame();
+ profile.addDataPoint('drawFrameDuration', _drawFrameStopwatch.elapsed,
+ reported: true);
+ }
+
+ if (shouldContinue()) {
+ showWidget = !showWidget;
+ _hostState._setStateTrampoline();
+ } else {
+ for (final VoidCallback fn in _didStopCallbacks) {
+ fn();
+ }
+ _runCompleter.complete();
+ }
+ }
+
+ @override
+ void _onError(dynamic error, StackTrace stackTrace) {
+ _runCompleter.completeError(error, stackTrace);
+ }
+
+ @override
+ Future<Profile> run() async {
+ _runCompleter = Completer<void>();
+ final Profile localProfile = profile = Profile(name: name);
+ final _RecordingWidgetsBinding binding =
+ _RecordingWidgetsBinding.ensureInitialized();
+ binding._beginRecording(this, _WidgetBuildRecorderHost(this));
+
+ try {
+ await _runCompleter.future;
+ return localProfile;
+ } finally {
+ _runCompleter = null;
+ profile = null;
+ }
+ }
+}
+
+/// Hosts widgets created by [WidgetBuildRecorder].
+class _WidgetBuildRecorderHost extends StatefulWidget {
+ const _WidgetBuildRecorderHost(this.recorder);
+
+ final WidgetBuildRecorder recorder;
+
+ @override
+ State<StatefulWidget> createState() => _WidgetBuildRecorderHostState();
+}
+
+class _WidgetBuildRecorderHostState extends State<_WidgetBuildRecorderHost> {
+ @override
+ void initState() {
+ super.initState();
+ widget.recorder._hostState = this;
+ }
+
+ // This is just to bypass the @protected on setState.
+ void _setStateTrampoline() {
+ setState(() {});
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox.expand(
+ child: widget.recorder._getWidgetForFrame(),
+ );
+ }
+}
+
+/// Series of time recordings indexed in time order.
+///
+/// It can calculate [average], [standardDeviation] and [noise]. If the amount
+/// of data collected is higher than [_kMeasuredSampleCount], then these
+/// calculations will only apply to the latest [_kMeasuredSampleCount] data
+/// points.
+class Timeseries {
+ /// Creates an empty timeseries.
+ ///
+ /// [name], [isReported], and [useCustomWarmUp] must not be null.
+ Timeseries(this.name, this.isReported, {this.useCustomWarmUp = false})
+ : _warmUpFrameCount = useCustomWarmUp ? 0 : null;
+
+ /// The label of this timeseries used for debugging and result inspection.
+ final String name;
+
+ /// Whether this timeseries is reported to the benchmark dashboard.
+ ///
+ /// If `true` a new benchmark card is created for the timeseries and is
+ /// visible on the dashboard.
+ ///
+ /// If `false` the data is stored but it does not show up on the dashboard.
+ /// Use unreported metrics for metrics that are useful for manual inspection
+ /// but that are too fine-grained to be useful for tracking on the dashboard.
+ final bool isReported;
+
+ /// Whether to delimit warm-up frames in a custom way.
+ final bool useCustomWarmUp;
+
+ /// The number of frames ignored as warm-up frames, used only
+ /// when [useCustomWarmUp] is true.
+ int _warmUpFrameCount;
+
+ /// The number of frames ignored as warm-up frames.
+ int get warmUpFrameCount =>
+ useCustomWarmUp ? _warmUpFrameCount : count - kMeasuredSampleCount;
+
+ /// List of all the values that have been recorded.
+ ///
+ /// This list has no limit.
+ final List<double> _allValues = <double>[];
+
+ /// The total amount of data collected, including ones that were dropped
+ /// because of the sample size limit.
+ int get count => _allValues.length;
+
+ /// Extracts useful statistics out of this timeseries.
+ ///
+ /// See [TimeseriesStats] for more details.
+ TimeseriesStats computeStats() {
+ final int finalWarmUpFrameCount = warmUpFrameCount;
+
+ assert(finalWarmUpFrameCount >= 0 && finalWarmUpFrameCount < count);
+
+ // The first few values we simply discard and never look at. They're from the warm-up phase.
+ final List<double> warmUpValues =
+ _allValues.sublist(0, finalWarmUpFrameCount);
+
+ // Values we analyze.
+ final List<double> candidateValues =
+ _allValues.sublist(finalWarmUpFrameCount);
+
+ // The average that includes outliers.
+ final double dirtyAverage = _computeAverage(name, candidateValues);
+
+ // The standard deviation that includes outliers.
+ final double dirtyStandardDeviation =
+ _computeStandardDeviationForPopulation(name, candidateValues);
+
+ // Any value that's higher than this is considered an outlier.
+ final double outlierCutOff = dirtyAverage + dirtyStandardDeviation;
+
+ // Candidates with outliers removed.
+ final Iterable<double> cleanValues =
+ candidateValues.where((double value) => value <= outlierCutOff);
+
+ // Outlier candidates.
+ final Iterable<double> outliers =
+ candidateValues.where((double value) => value > outlierCutOff);
+
+ // Final statistics.
+ final double cleanAverage = _computeAverage(name, cleanValues);
+ final double standardDeviation =
+ _computeStandardDeviationForPopulation(name, cleanValues);
+ final double noise =
+ cleanAverage > 0.0 ? standardDeviation / cleanAverage : 0.0;
+
+ // Compute outlier average. If there are no outliers the outlier average is
+ // the same as clean value average. In other words, in a perfect benchmark
+ // with no noise the difference between average and outlier average is zero,
+ // which the best possible outcome. Noise produces a positive difference
+ // between the two.
+ final double outlierAverage =
+ outliers.isNotEmpty ? _computeAverage(name, outliers) : cleanAverage;
+
+ final List<AnnotatedSample> annotatedValues = <AnnotatedSample>[
+ for (final double warmUpValue in warmUpValues)
+ AnnotatedSample(
+ magnitude: warmUpValue,
+ isOutlier: warmUpValue > outlierCutOff,
+ isWarmUpValue: true,
+ ),
+ for (final double candidate in candidateValues)
+ AnnotatedSample(
+ magnitude: candidate,
+ isOutlier: candidate > outlierCutOff,
+ isWarmUpValue: false,
+ ),
+ ];
+
+ return TimeseriesStats(
+ name: name,
+ average: cleanAverage,
+ outlierCutOff: outlierCutOff,
+ outlierAverage: outlierAverage,
+ standardDeviation: standardDeviation,
+ noise: noise,
+ cleanSampleCount: cleanValues.length,
+ outlierSampleCount: outliers.length,
+ samples: annotatedValues,
+ );
+ }
+
+ /// Adds a value to this timeseries.
+ void add(double value, {@required bool isWarmUpValue}) {
+ if (value < 0.0) {
+ throw StateError(
+ 'Timeseries $name: negative metric values are not supported. Got: $value',
+ );
+ }
+ _allValues.add(value);
+ if (useCustomWarmUp && isWarmUpValue) {
+ _warmUpFrameCount += 1;
+ }
+ }
+}
+
+/// Various statistics about a [Timeseries].
+///
+/// See the docs on the individual fields for more details.
+@sealed
+class TimeseriesStats {
+ /// Creates statistics for a time series.
+ const TimeseriesStats({
+ @required this.name,
+ @required this.average,
+ @required this.outlierCutOff,
+ @required this.outlierAverage,
+ @required this.standardDeviation,
+ @required this.noise,
+ @required this.cleanSampleCount,
+ @required this.outlierSampleCount,
+ @required this.samples,
+ });
+
+ /// The label used to refer to the corresponding timeseries.
+ final String name;
+
+ /// The average value of the measured samples without outliers.
+ final double average;
+
+ /// The standard deviation in the measured samples without outliers.
+ final double standardDeviation;
+
+ /// The noise as a multiple of the [average] value takes from clean samples.
+ ///
+ /// This value can be multiplied by 100.0 to get noise as a percentage of
+ /// the average.
+ ///
+ /// If [average] is zero, treats the result as perfect score, returns zero.
+ final double noise;
+
+ /// The maximum value a sample can have without being considered an outlier.
+ ///
+ /// See [Timeseries.computeStats] for details on how this value is computed.
+ final double outlierCutOff;
+
+ /// The average of outlier samples.
+ ///
+ /// This value can be used to judge how badly we jank, when we jank.
+ ///
+ /// Another useful metrics is the difference between [outlierAverage] and
+ /// [average]. The smaller the value the more predictable is the performance
+ /// of the corresponding benchmark.
+ final double outlierAverage;
+
+ /// The number of measured samples after outlier are removed.
+ final int cleanSampleCount;
+
+ /// The number of outliers.
+ final int outlierSampleCount;
+
+ /// All collected samples, annotated with statistical information.
+ ///
+ /// See [AnnotatedSample] for more details.
+ final List<AnnotatedSample> samples;
+
+ /// Outlier average divided by clean average.
+ ///
+ /// This is a measure of performance consistency. The higher this number the
+ /// worse is jank when it happens. Smaller is better, with 1.0 being the
+ /// perfect score. If [average] is zero, this value defaults to 1.0.
+ double get outlierRatio => average > 0.0
+ ? outlierAverage / average
+ : 1.0; // this can only happen in perfect benchmark that reports only zeros
+
+ @override
+ String toString() {
+ final StringBuffer buffer = StringBuffer();
+ buffer.writeln(
+ '$name: (samples: $cleanSampleCount clean/$outlierSampleCount '
+ 'outliers/${cleanSampleCount + outlierSampleCount} '
+ 'measured/${samples.length} total)',
+ );
+ buffer.writeln(' | average: $average μs');
+ buffer.writeln(' | outlier average: $outlierAverage μs');
+ buffer.writeln(' | outlier/clean ratio: ${outlierRatio}x');
+ buffer.writeln(' | noise: ${_ratioToPercent(noise)}');
+ return buffer.toString();
+ }
+}
+
+/// Annotates a single measurement with statistical information.
+@sealed
+class AnnotatedSample {
+ /// Creates an annotated measurement sample.
+ const AnnotatedSample({
+ @required this.magnitude,
+ @required this.isOutlier,
+ @required this.isWarmUpValue,
+ });
+
+ /// The non-negative raw result of the measurement.
+ final double magnitude;
+
+ /// Whether this sample was considered an outlier.
+ final bool isOutlier;
+
+ /// Whether this sample was taken during the warm-up phase.
+ ///
+ /// If this value is `true`, this sample does not participate in
+ /// statistical computations. However, the sample would still be
+ /// shown in the visualization of results so that the benchmark
+ /// can be inspected manually to make sure there's a predictable
+ /// warm-up regression slope.
+ final bool isWarmUpValue;
+}
+
+/// Base class for a profile collected from running a benchmark.
+class Profile {
+ /// Creates an empty profile.
+ ///
+ /// [name] and [useCustomWarmUp] must not be null.
+ Profile({@required this.name, this.useCustomWarmUp = false})
+ : assert(name != null),
+ _isWarmingUp = useCustomWarmUp;
+
+ /// The name of the benchmark that produced this profile.
+ final String name;
+
+ /// Whether to delimit warm-up frames in a custom way.
+ final bool useCustomWarmUp;
+
+ /// Whether we are measuring warm-up frames currently.
+ bool get isWarmingUp => _isWarmingUp;
+
+ bool _isWarmingUp;
+
+ /// Stop the warm-up phase.
+ ///
+ /// Call this method only when [useCustomWarmUp] and [isWarmingUp] are both
+ /// true.
+ /// Call this method only once for each profile.
+ void stopWarmingUp() {
+ if (!useCustomWarmUp) {
+ throw Exception(
+ '`stopWarmingUp` should be used only when `useCustomWarmUp` is true.');
+ } else if (!_isWarmingUp) {
+ throw Exception('Warm-up already stopped.');
+ } else {
+ _isWarmingUp = false;
+ }
+ }
+
+ /// This data will be used to display cards in the Flutter Dashboard.
+ final Map<String, Timeseries> scoreData = <String, Timeseries>{};
+
+ /// This data isn't displayed anywhere. It's stored for completeness purposes.
+ final Map<String, dynamic> extraData = <String, dynamic>{};
+
+ /// Invokes [callback] and records the duration of its execution under [key].
+ Duration record(String key, VoidCallback callback,
+ {@required bool reported}) {
+ final Duration duration = timeAction(callback);
+ addDataPoint(key, duration, reported: reported);
+ return duration;
+ }
+
+ /// Adds a timed sample to the timeseries corresponding to [key].
+ ///
+ /// Set [reported] to `true` to report the timeseries to the dashboard UI.
+ ///
+ /// Set [reported] to `false` to store the data, but not show it on the
+ /// dashboard UI.
+ void addDataPoint(String key, Duration duration, {@required bool reported}) {
+ scoreData
+ .putIfAbsent(
+ key,
+ () => Timeseries(key, reported, useCustomWarmUp: useCustomWarmUp),
+ )
+ .add(duration.inMicroseconds.toDouble(), isWarmUpValue: isWarmingUp);
+ }
+
+ /// Decides whether the data collected so far is sufficient to stop, or
+ /// whether the benchmark should continue collecting more data.
+ ///
+ /// The signals used are sample size, noise, and duration.
+ ///
+ /// If any of the timeseries doesn't satisfy the noise requirements, this
+ /// method will return true (asking the benchmark to continue collecting
+ /// data).
+ bool shouldContinue() {
+ // If there are no `Timeseries` in the `scoreData`, then we haven't
+ // recorded anything yet. Don't stop.
+ if (scoreData.isEmpty) {
+ return true;
+ }
+
+ // We have recorded something, but do we have enough samples? If every
+ // timeseries has collected enough samples, stop the benchmark.
+ return !scoreData.keys
+ .every((String key) => scoreData[key].count >= kTotalSampleCount);
+ }
+
+ /// Returns a JSON representation of the profile that will be sent to the
+ /// server.
+ Map<String, dynamic> toJson() {
+ final List<String> scoreKeys = <String>[];
+ final Map<String, dynamic> json = <String, dynamic>{
+ 'name': name,
+ 'scoreKeys': scoreKeys,
+ };
+
+ for (final String key in scoreData.keys) {
+ final Timeseries timeseries = scoreData[key];
+
+ if (timeseries.isReported) {
+ scoreKeys.add('$key.average');
+ // Report `outlierRatio` rather than `outlierAverage`, because
+ // the absolute value of outliers is less interesting than the
+ // ratio.
+ scoreKeys.add('$key.outlierRatio');
+ }
+
+ final TimeseriesStats stats = timeseries.computeStats();
+ json['$key.average'] = stats.average;
+ json['$key.outlierAverage'] = stats.outlierAverage;
+ json['$key.outlierRatio'] = stats.outlierRatio;
+ json['$key.noise'] = stats.noise;
+ }
+
+ json.addAll(extraData);
+
+ return json;
+ }
+
+ @override
+ String toString() {
+ final StringBuffer buffer = StringBuffer();
+ buffer.writeln('name: $name');
+ for (final String key in scoreData.keys) {
+ final Timeseries timeseries = scoreData[key];
+ final TimeseriesStats stats = timeseries.computeStats();
+ buffer.writeln(stats.toString());
+ }
+ for (final String key in extraData.keys) {
+ final dynamic value = extraData[key];
+ if (value is List) {
+ buffer.writeln('$key:');
+ for (final dynamic item in value) {
+ buffer.writeln(' - $item');
+ }
+ } else {
+ buffer.writeln('$key: $value');
+ }
+ }
+ return buffer.toString();
+ }
+}
+
+/// Computes the arithmetic mean (or average) of given [values].
+double _computeAverage(String label, Iterable<double> values) {
+ if (values.isEmpty) {
+ throw StateError(
+ '$label: attempted to compute an average of an empty value list.');
+ }
+
+ final double sum = values.reduce((double a, double b) => a + b);
+ return sum / values.length;
+}
+
+/// Computes population standard deviation.
+///
+/// Unlike sample standard deviation, which divides by N - 1, this divides by N.
+///
+/// See also:
+///
+/// * https://en.wikipedia.org/wiki/Standard_deviation
+double _computeStandardDeviationForPopulation(
+ String label, Iterable<double> population) {
+ if (population.isEmpty) {
+ throw StateError(
+ '$label: attempted to compute the standard deviation of empty population.');
+ }
+ final double mean = _computeAverage(label, population);
+ final double sumOfSquaredDeltas = population.fold<double>(
+ 0.0,
+ (double previous, double value) => previous += math.pow(value - mean, 2),
+ );
+ return math.sqrt(sumOfSquaredDeltas / population.length);
+}
+
+String _ratioToPercent(double value) {
+ return '${(value * 100).toStringAsFixed(2)}%';
+}
+
+/// Implemented by recorders that use [_RecordingWidgetsBinding] to receive
+/// frame life-cycle calls.
+abstract class FrameRecorder {
+ /// Add a callback that will be called by the recorder when it stops recording.
+ void registerDidStop(VoidCallback cb);
+
+ /// Called just before calling [SchedulerBinding.handleDrawFrame].
+ void frameWillDraw();
+
+ /// Called immediately after calling [SchedulerBinding.handleDrawFrame].
+ void frameDidDraw();
+
+ /// Reports an error.
+ ///
+ /// The implementation is expected to halt benchmark execution as soon as possible.
+ void _onError(dynamic error, StackTrace stackTrace);
+}
+
+/// A variant of [WidgetsBinding] that collaborates with a [Recorder] to decide
+/// when to stop pumping frames.
+///
+/// A normal [WidgetsBinding] typically always pumps frames whenever a widget
+/// instructs it to do so by calling [scheduleFrame] (transitively via
+/// `setState`). This binding will stop pumping new frames as soon as benchmark
+/// parameters are satisfactory (e.g. when the metric noise levels become low
+/// enough).
+class _RecordingWidgetsBinding extends BindingBase
+ with
+ GestureBinding,
+ SchedulerBinding,
+ ServicesBinding,
+ PaintingBinding,
+ SemanticsBinding,
+ RendererBinding,
+ WidgetsBinding {
+ /// Makes an instance of [_RecordingWidgetsBinding] the current binding.
+ static _RecordingWidgetsBinding ensureInitialized() {
+ if (WidgetsBinding.instance == null) {
+ _RecordingWidgetsBinding();
+ }
+ return WidgetsBinding.instance;
+ }
+
+ FrameRecorder _recorder;
+ bool _hasErrored = false;
+
+ /// To short-circuit all frame lifecycle methods when the benchmark has
+ /// stopped collecting data.
+ bool _benchmarkStopped = false;
+
+ void _beginRecording(FrameRecorder recorder, Widget widget) {
+ if (_recorder != null) {
+ throw Exception(
+ 'Cannot call _RecordingWidgetsBinding._beginRecording more than once',
+ );
+ }
+ final FlutterExceptionHandler originalOnError = FlutterError.onError;
+
+ recorder.registerDidStop(() {
+ _benchmarkStopped = true;
+ });
+
+ // Fail hard and fast on errors. Benchmarks should not have any errors.
+ FlutterError.onError = (FlutterErrorDetails details) {
+ _haltBenchmarkWithError(details.exception, details.stack);
+ originalOnError(details);
+ };
+ _recorder = recorder;
+ runApp(widget);
+ }
+
+ void _haltBenchmarkWithError(dynamic error, StackTrace stackTrace) {
+ if (_hasErrored) {
+ return;
+ }
+ _recorder._onError(error, stackTrace);
+ _hasErrored = true;
+ }
+
+ @override
+ void handleBeginFrame(Duration rawTimeStamp) {
+ // Don't keep on truckin' if there's an error or the benchmark has stopped.
+ if (_hasErrored || _benchmarkStopped) {
+ return;
+ }
+ try {
+ super.handleBeginFrame(rawTimeStamp);
+ } catch (error, stackTrace) {
+ _haltBenchmarkWithError(error, stackTrace);
+ rethrow;
+ }
+ }
+
+ @override
+ void scheduleFrame() {
+ // Don't keep on truckin' if there's an error or the benchmark has stopped.
+ if (_hasErrored || _benchmarkStopped) {
+ return;
+ }
+ super.scheduleFrame();
+ }
+
+ @override
+ void handleDrawFrame() {
+ // Don't keep on truckin' if there's an error or the benchmark has stopped.
+ if (_hasErrored || _benchmarkStopped) {
+ return;
+ }
+ try {
+ _recorder.frameWillDraw();
+ super.handleDrawFrame();
+ _recorder.frameDidDraw();
+ } catch (error, stackTrace) {
+ _haltBenchmarkWithError(error, stackTrace);
+ rethrow;
+ }
+ }
+}
+
+int _currentFrameNumber = 1;
+
+/// If [_calledStartMeasureFrame] is true, we have called [startMeasureFrame]
+/// but have not its pairing [endMeasureFrame] yet.
+///
+/// This flag ensures that [startMeasureFrame] and [endMeasureFrame] are always
+/// called in pairs, with [startMeasureFrame] followed by [endMeasureFrame].
+bool _calledStartMeasureFrame = false;
+
+/// Whether we are recording a measured frame.
+///
+/// This flag ensures that we always stop measuring a frame if we
+/// have started one. Because we want to skip warm-up frames, this flag
+/// is necessary.
+bool _isMeasuringFrame = false;
+
+/// Adds a marker indication the beginning of frame rendering.
+///
+/// This adds an event to the performance trace used to find measured frames in
+/// Chrome tracing data. The tracing data contains all frames, but some
+/// benchmarks are only interested in a subset of frames. For example,
+/// [WidgetBuildRecorder] only measures frames that build widgets, and ignores
+/// frames that clear the screen.
+///
+/// Warm-up frames are not measured. If [profile.isWarmingUp] is true,
+/// this function does nothing.
+void startMeasureFrame(Profile profile) {
+ if (_calledStartMeasureFrame) {
+ throw Exception('`startMeasureFrame` called twice in a row.');
+ }
+
+ _calledStartMeasureFrame = true;
+
+ if (!profile.isWarmingUp) {
+ // Tell the browser to mark the beginning of the frame.
+ html.window.performance.mark('measured_frame_start#$_currentFrameNumber');
+
+ _isMeasuringFrame = true;
+ }
+}
+
+/// Signals the end of a measured frame.
+///
+/// See [startMeasureFrame] for details on what this instrumentation is used
+/// for.
+///
+/// Warm-up frames are not measured. If [profile.isWarmingUp] was true
+/// when the corresponding [startMeasureFrame] was called,
+/// this function does nothing.
+void endMeasureFrame() {
+ if (!_calledStartMeasureFrame) {
+ throw Exception(
+ '`startMeasureFrame` has not been called before calling `endMeasureFrame`');
+ }
+
+ _calledStartMeasureFrame = false;
+
+ if (_isMeasuringFrame) {
+ // Tell the browser to mark the end of the frame, and measure the duration.
+ html.window.performance.mark('measured_frame_end#$_currentFrameNumber');
+ html.window.performance.measure(
+ 'measured_frame',
+ 'measured_frame_start#$_currentFrameNumber',
+ 'measured_frame_end#$_currentFrameNumber',
+ );
+
+ // Increment the current frame number.
+ _currentFrameNumber += 1;
+
+ _isMeasuringFrame = false;
+ }
+}
+
+/// A function that receives a benchmark value from the framework.
+typedef EngineBenchmarkValueListener = void Function(num value);
+
+// Maps from a value label name to a listener.
+final Map<String, EngineBenchmarkValueListener> _engineBenchmarkListeners =
+ <String, EngineBenchmarkValueListener>{};
+
+/// Registers a [listener] for engine benchmark values labeled by [name].
+///
+/// If another listener is already registered, overrides it.
+void registerEngineBenchmarkValueListener(
+ String name, EngineBenchmarkValueListener listener) {
+ if (listener == null) {
+ throw ArgumentError(
+ 'Listener must not be null. To stop listening to engine benchmark values '
+ 'under label "$name", call stopListeningToEngineBenchmarkValues(\'$name\').',
+ );
+ }
+
+ if (_engineBenchmarkListeners.containsKey(name)) {
+ throw StateError('A listener for "$name" is already registered.\n'
+ 'Call `stopListeningToEngineBenchmarkValues` to unregister the previous '
+ 'listener before registering a new one.');
+ }
+
+ if (_engineBenchmarkListeners.isEmpty) {
+ // The first listener is being registered. Register the global listener.
+ js_util.setProperty(html.window, '_flutter_internal_on_benchmark',
+ _dispatchEngineBenchmarkValue);
+ }
+
+ _engineBenchmarkListeners[name] = listener;
+}
+
+/// Stops listening to engine benchmark values under labeled by [name].
+void stopListeningToEngineBenchmarkValues(String name) {
+ _engineBenchmarkListeners.remove(name);
+ if (_engineBenchmarkListeners.isEmpty) {
+ // The last listener unregistered. Remove the global listener.
+ js_util.setProperty(html.window, '_flutter_internal_on_benchmark', null);
+ }
+}
+
+// Dispatches a benchmark value reported by the engine to the relevant listener.
+//
+// If there are no listeners registered for [name], ignores the value.
+void _dispatchEngineBenchmarkValue(String name, double value) {
+ final EngineBenchmarkValueListener listener = _engineBenchmarkListeners[name];
+ if (listener != null) {
+ listener(value);
+ }
+}
diff --git a/packages/web_benchmarks/lib/src/runner.dart b/packages/web_benchmarks/lib/src/runner.dart
new file mode 100644
index 0000000..278d6be
--- /dev/null
+++ b/packages/web_benchmarks/lib/src/runner.dart
@@ -0,0 +1,318 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert' show json;
+import 'dart:io' as io;
+
+import 'package:logging/logging.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as path;
+import 'package:process/process.dart';
+import 'package:shelf/shelf.dart';
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'package:shelf_static/shelf_static.dart';
+
+import 'benchmark_result.dart';
+import 'browser.dart';
+import 'common.dart';
+
+/// The default port number used by the local benchmark server.
+const int defaultBenchmarkServerPort = 9999;
+
+/// The default port number used for Chrome DevTool Protocol.
+const int defaultChromeDebugPort = 10000;
+
+/// Builds and serves a Flutter Web app, collects raw benchmark data and
+/// summarizes the result as a [BenchmarkResult].
+class BenchmarkServer {
+ /// Creates a benchmark server.
+ ///
+ /// [benchmarkAppDirectory] is the directory containing the app that's being
+ /// benchmarked. The app is expected to use `package:web_benchmarks/client.dart`
+ /// and call the `runBenchmarks` function to run the benchmarks.
+ ///
+ /// [entryPoint] is the path to the main app file that runs the benchmark. It
+ /// can be different (and typically is) from the production entry point of the
+ /// app.
+ ///
+ /// If [useCanvasKit] is true, builds the app in CanvasKit mode.
+ ///
+ /// [benchmarkServerPort] is the port this benchmark server serves the app on.
+ ///
+ /// [chromeDebugPort] is the port Chrome uses for DevTool Protocol used to
+ /// extract tracing data.
+ ///
+ /// If [headless] is true, runs Chrome without UI. In particular, this is
+ /// useful in environments (e.g. CI) that doesn't have a display.
+ BenchmarkServer({
+ @required this.benchmarkAppDirectory,
+ @required this.entryPoint,
+ @required this.useCanvasKit,
+ @required this.benchmarkServerPort,
+ @required this.chromeDebugPort,
+ @required this.headless,
+ });
+
+ final ProcessManager _processManager = const LocalProcessManager();
+
+ /// The directory containing the app that's being benchmarked.
+ ///
+ /// The app is expected to use `package:web_benchmarks/client.dart`
+ /// and call the `runBenchmarks` function to run the benchmarks.
+ final io.Directory benchmarkAppDirectory;
+
+ /// The path to the main app file that runs the benchmark.
+ ///
+ /// It can be different (and typically is) from the production entry point of
+ /// the app.
+ final String entryPoint;
+
+ /// Whether to build the app in CanvasKit mode.
+ final bool useCanvasKit;
+
+ /// The port this benchmark server serves the app on.
+ final int benchmarkServerPort;
+
+ /// The port Chrome uses for DevTool Protocol used to extract tracing data.
+ final int chromeDebugPort;
+
+ /// Whether to run Chrome without UI.
+ ///
+ /// This is useful in environments (e.g. CI) that doesn't have a display.
+ final bool headless;
+
+ /// Builds and serves the benchmark app, and collects benchmark results.
+ Future<BenchmarkResults> run() async {
+ // Reduce logging level. Otherwise, package:webkit_inspection_protocol is way too spammy.
+ Logger.root.level = Level.INFO;
+
+ if (!_processManager.canRun('flutter')) {
+ throw Exception(
+ 'flutter executable is not runnable. Make sure it\'s in the PATH.');
+ }
+
+ final io.ProcessResult buildResult = await _processManager.run(
+ <String>[
+ 'flutter',
+ 'build',
+ 'web',
+ '--dart-define=FLUTTER_WEB_ENABLE_PROFILING=true',
+ if (useCanvasKit) '--dart-define=FLUTTER_WEB_USE_SKIA=true',
+ '--profile',
+ '-t',
+ entryPoint,
+ ],
+ workingDirectory: benchmarkAppDirectory.path,
+ );
+
+ if (buildResult.exitCode != 0) {
+ io.stderr.writeln(buildResult.stdout);
+ io.stderr.writeln(buildResult.stderr);
+ throw Exception('Failed to build the benchmark.');
+ }
+
+ final Completer<List<Map<String, dynamic>>> profileData =
+ Completer<List<Map<String, dynamic>>>();
+ final List<Map<String, dynamic>> collectedProfiles =
+ <Map<String, dynamic>>[];
+ List<String> benchmarks;
+ Iterator<String> benchmarkIterator;
+
+ // This future fixes a race condition between the web-page loading and
+ // asking to run a benchmark, and us connecting to Chrome's DevTools port.
+ // Sometime one wins. Other times, the other wins.
+ Future<Chrome> whenChromeIsReady;
+ Chrome chrome;
+ io.HttpServer server;
+ List<Map<String, dynamic>> latestPerformanceTrace;
+ Cascade cascade = Cascade();
+
+ // Serves the static files built for the app (html, js, images, fonts, etc)
+ cascade = cascade.add(createStaticHandler(
+ path.join(benchmarkAppDirectory.path, 'build', 'web'),
+ defaultDocument: 'index.html',
+ ));
+
+ // Serves the benchmark server API used by the benchmark app to coordinate
+ // the running of benchmarks.
+ cascade = cascade.add((Request request) async {
+ try {
+ chrome ??= await whenChromeIsReady;
+ if (request.requestedUri.path.endsWith('/profile-data')) {
+ final Map<String, dynamic> profile =
+ json.decode(await request.readAsString());
+ final String benchmarkName = profile['name'];
+ if (benchmarkName != benchmarkIterator.current) {
+ profileData.completeError(Exception(
+ 'Browser returned benchmark results from a wrong benchmark.\n'
+ 'Requested to run bechmark ${benchmarkIterator.current}, but '
+ 'got results for $benchmarkName.',
+ ));
+ server.close();
+ }
+
+ // Trace data is null when the benchmark is not frame-based, such as RawRecorder.
+ if (latestPerformanceTrace != null) {
+ final BlinkTraceSummary traceSummary =
+ BlinkTraceSummary.fromJson(latestPerformanceTrace);
+ profile['totalUiFrame.average'] =
+ traceSummary.averageTotalUIFrameTime.inMicroseconds;
+ profile['scoreKeys'] ??=
+ <dynamic>[]; // using dynamic for consistency with JSON
+ profile['scoreKeys'].add('totalUiFrame.average');
+ latestPerformanceTrace = null;
+ }
+ collectedProfiles.add(profile);
+ return Response.ok('Profile received');
+ } else if (request.requestedUri.path
+ .endsWith('/start-performance-tracing')) {
+ latestPerformanceTrace = null;
+ await chrome.beginRecordingPerformance(
+ request.requestedUri.queryParameters['label']);
+ return Response.ok('Started performance tracing');
+ } else if (request.requestedUri.path
+ .endsWith('/stop-performance-tracing')) {
+ latestPerformanceTrace = await chrome.endRecordingPerformance();
+ return Response.ok('Stopped performance tracing');
+ } else if (request.requestedUri.path.endsWith('/on-error')) {
+ final Map<String, dynamic> errorDetails =
+ json.decode(await request.readAsString());
+ server.close();
+ // Keep the stack trace as a string. It's thrown in the browser, not this Dart VM.
+ final String errorMessage =
+ 'Caught browser-side error: ${errorDetails['error']}\n${errorDetails['stackTrace']}';
+ if (!profileData.isCompleted) {
+ profileData.completeError(errorMessage);
+ } else {
+ io.stderr.writeln(errorMessage);
+ }
+ return Response.ok('');
+ } else if (request.requestedUri.path.endsWith('/next-benchmark')) {
+ if (benchmarks == null) {
+ benchmarks =
+ (json.decode(await request.readAsString())).cast<String>();
+ benchmarkIterator = benchmarks.iterator;
+ }
+ if (benchmarkIterator.moveNext()) {
+ final String nextBenchmark = benchmarkIterator.current;
+ print('Launching benchmark "$nextBenchmark"');
+ return Response.ok(nextBenchmark);
+ } else {
+ profileData.complete(collectedProfiles);
+ return Response.ok(kEndOfBenchmarks);
+ }
+ } else if (request.requestedUri.path.endsWith('/print-to-console')) {
+ // A passthrough used by
+ // `dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart`
+ // to print information.
+ final String message = await request.readAsString();
+ print('[APP] $message');
+ return Response.ok('Reported.');
+ } else {
+ return Response.notFound(
+ 'This request is not handled by the profile-data handler.');
+ }
+ } catch (error, stackTrace) {
+ if (!profileData.isCompleted) {
+ profileData.completeError(error, stackTrace);
+ } else {
+ io.stderr.writeln('Caught error: $error');
+ io.stderr.writeln('$stackTrace');
+ }
+ return Response.internalServerError(body: '$error');
+ }
+ });
+
+ // If all previous handlers returned HTTP 404, this is the last handler
+ // that simply warns about the unrecognized path.
+ cascade = cascade.add((Request request) {
+ io.stderr.writeln('Unrecognized URL path: ${request.requestedUri.path}');
+ return Response.notFound('Not found: ${request.requestedUri.path}');
+ });
+
+ server = await io.HttpServer.bind('localhost', benchmarkServerPort);
+ try {
+ shelf_io.serveRequests(server, cascade.handler);
+
+ final String dartToolDirectory =
+ path.join(benchmarkAppDirectory.path, '.dart_tool');
+ final String userDataDir = io.Directory(dartToolDirectory)
+ .createTempSync('chrome_user_data_')
+ .path;
+
+ final ChromeOptions options = ChromeOptions(
+ url: 'http://localhost:$benchmarkServerPort/index.html',
+ userDataDirectory: userDataDir,
+ windowHeight: 1024,
+ windowWidth: 1024,
+ headless: headless,
+ debugPort: chromeDebugPort,
+ );
+
+ print('Launching Chrome.');
+ whenChromeIsReady = Chrome.launch(
+ options,
+ onError: (String error) {
+ if (!profileData.isCompleted) {
+ profileData.completeError(Exception(error));
+ } else {
+ io.stderr.writeln('Chrome error: $error');
+ }
+ },
+ workingDirectory: benchmarkAppDirectory.path,
+ );
+
+ print('Waiting for the benchmark to report benchmark profile.');
+ final List<Map<String, dynamic>> profiles = await profileData.future;
+
+ print('Received profile data');
+ final Map<String, List<BenchmarkScore>> results =
+ <String, List<BenchmarkScore>>{};
+ for (final Map<String, dynamic> profile in profiles) {
+ final String benchmarkName = profile['name'];
+ if (benchmarkName.isEmpty) {
+ throw 'Benchmark name is empty';
+ }
+
+ final List<String> scoreKeys = List<String>.from(profile['scoreKeys']);
+ if (scoreKeys == null || scoreKeys.isEmpty) {
+ throw 'No score keys in benchmark "$benchmarkName"';
+ }
+ for (final String scoreKey in scoreKeys) {
+ if (scoreKey == null || scoreKey.isEmpty) {
+ throw 'Score key is empty in benchmark "$benchmarkName". '
+ 'Received [${scoreKeys.join(', ')}]';
+ }
+ }
+
+ final List<BenchmarkScore> scores = <BenchmarkScore>[];
+ for (final String key in profile.keys) {
+ if (key == 'name' || key == 'scoreKeys') {
+ continue;
+ }
+ scores.add(BenchmarkScore(
+ metric: key,
+ value: profile[key],
+ ));
+ }
+ results[benchmarkName] = scores;
+ }
+ return BenchmarkResults(results);
+ } finally {
+ if (headless) {
+ chrome?.stop();
+ } else {
+ // In non-headless mode wait for the developer to close Chrome
+ // manually. Otherwise, they won't get a chance to debug anything.
+ print(
+ 'Benchmark finished. Chrome running in windowed mode. Close '
+ 'Chrome manually to continue.',
+ );
+ await chrome?.whenExits;
+ }
+ server?.close();
+ }
+ }
+}
diff --git a/packages/web_benchmarks/pubspec.yaml b/packages/web_benchmarks/pubspec.yaml
new file mode 100644
index 0000000..7397ed1
--- /dev/null
+++ b/packages/web_benchmarks/pubspec.yaml
@@ -0,0 +1,23 @@
+name: web_benchmarks
+description: A benchmark harness for performance-testing Flutter apps in Chrome.
+version: 0.0.5
+homepage: https://github.com/flutter/packages/tree/master/packages/web_benchmarks
+
+environment:
+ sdk: ">=2.7.0 <3.0.0"
+ flutter: ">=1.17.0"
+
+# Using +2 upper limit on some packages to allow null-safe versions
+dependencies:
+ flutter:
+ sdk: flutter
+ flutter_test:
+ sdk: flutter
+ logging: ">=0.11.4 <2.0.0"
+ meta: ">=1.0.0 <2.0.0"
+ path: ">=1.7.0 <2.0.0"
+ process: ">=3.0.13 <5.0.0"
+ shelf: ">=0.7.5 <2.0.0"
+ shelf_static: ">=0.2.8 <2.0.0"
+ test: ">=1.15.0 <3.0.0"
+ webkit_inspection_protocol: ">=0.7.3 <2.0.0"
diff --git a/packages/web_benchmarks/testing/test_app/.gitignore b/packages/web_benchmarks/testing/test_app/.gitignore
new file mode 100644
index 0000000..9d532b1
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/.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/web_benchmarks/testing/test_app/.metadata b/packages/web_benchmarks/testing/test_app/.metadata
new file mode 100644
index 0000000..5e875f2
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/.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: d26268bb9e6d713a73d6148da7fa75936d442741
+ channel: master
+
+project_type: app
diff --git a/packages/web_benchmarks/testing/test_app/README.md b/packages/web_benchmarks/testing/test_app/README.md
new file mode 100644
index 0000000..c3bb8e4
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/README.md
@@ -0,0 +1,3 @@
+# test_app
+
+An example app for web benchmarks testing.
diff --git a/packages/web_benchmarks/testing/test_app/analysis_options.yaml b/packages/web_benchmarks/testing/test_app/analysis_options.yaml
new file mode 100644
index 0000000..2597fd1
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/analysis_options.yaml
@@ -0,0 +1,7 @@
+include: ../../../../analysis_options.yaml
+
+linter:
+ rules:
+ # This is test code. Do not enforce docs.
+ package_api_docs: false
+ public_member_api_docs: false
diff --git a/packages/web_benchmarks/testing/test_app/lib/aboutpage.dart b/packages/web_benchmarks/testing/test_app/lib/aboutpage.dart
new file mode 100644
index 0000000..5844dfb
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/lib/aboutpage.dart
@@ -0,0 +1,29 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+const ValueKey<String> backKey = ValueKey<String>('backKey');
+
+class AboutPage extends StatelessWidget {
+ const AboutPage({Key key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ leading: BackButton(
+ key: backKey,
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ ),
+ body: Center(
+ child: Text(
+ 'This is a sample app.',
+ style: Theme.of(context).textTheme.headline3,
+ ),
+ ),
+ );
+ }
+}
diff --git a/packages/web_benchmarks/testing/test_app/lib/benchmarks/runner.dart b/packages/web_benchmarks/testing/test_app/lib/benchmarks/runner.dart
new file mode 100644
index 0000000..13daba1
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/lib/benchmarks/runner.dart
@@ -0,0 +1,99 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:web_benchmarks/client.dart';
+
+import '../aboutpage.dart' show backKey;
+import '../homepage.dart' show textKey, aboutPageKey;
+import '../main.dart';
+
+/// A recorder that measures frame building durations.
+abstract class AppRecorder extends WidgetRecorder {
+ AppRecorder({@required this.benchmarkName}) : super(name: benchmarkName);
+
+ final String benchmarkName;
+
+ Future<void> automate();
+
+ @override
+ Widget createWidget() {
+ Future<void>.delayed(const Duration(milliseconds: 400), automate);
+ return const MyApp();
+ }
+
+ Future<void> animationStops() async {
+ while (WidgetsBinding.instance.hasScheduledFrame) {
+ await Future<void>.delayed(const Duration(milliseconds: 200));
+ }
+ }
+}
+
+class ScrollRecorder extends AppRecorder {
+ ScrollRecorder() : super(benchmarkName: 'scroll');
+
+ @override
+ Future<void> automate() async {
+ final ScrollableState scrollable =
+ Scrollable.of(find.byKey(textKey).evaluate().single);
+ await scrollable.position.animateTo(
+ 30000,
+ curve: Curves.linear,
+ duration: const Duration(seconds: 20),
+ );
+ }
+}
+
+class PageRecorder extends AppRecorder {
+ PageRecorder() : super(benchmarkName: 'page');
+
+ bool _completed = false;
+
+ @override
+ bool shouldContinue() => profile.shouldContinue() || !_completed;
+
+ @override
+ Future<void> automate() async {
+ final LiveWidgetController controller =
+ LiveWidgetController(WidgetsBinding.instance);
+ for (int i = 0; i < 10; ++i) {
+ print('Testing round $i...');
+ await controller.tap(find.byKey(aboutPageKey));
+ await animationStops();
+ await controller.tap(find.byKey(backKey));
+ await animationStops();
+ }
+ _completed = true;
+ }
+}
+
+class TapRecorder extends AppRecorder {
+ TapRecorder() : super(benchmarkName: 'tap');
+
+ bool _completed = false;
+
+ @override
+ bool shouldContinue() => profile.shouldContinue() || !_completed;
+
+ @override
+ Future<void> automate() async {
+ final LiveWidgetController controller =
+ LiveWidgetController(WidgetsBinding.instance);
+ for (int i = 0; i < 10; ++i) {
+ print('Testing round $i...');
+ await controller.tap(find.byIcon(Icons.add));
+ await animationStops();
+ }
+ _completed = true;
+ }
+}
+
+Future<void> main() async {
+ await runBenchmarks(<String, RecorderFactory>{
+ 'scroll': () => ScrollRecorder(),
+ 'page': () => PageRecorder(),
+ 'tap': () => TapRecorder(),
+ });
+}
diff --git a/packages/web_benchmarks/testing/test_app/lib/homepage.dart b/packages/web_benchmarks/testing/test_app/lib/homepage.dart
new file mode 100644
index 0000000..7c7ac21
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/lib/homepage.dart
@@ -0,0 +1,91 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+const ValueKey<String> textKey = ValueKey<String>('textKey');
+const ValueKey<String> aboutPageKey = ValueKey<String>('aboutPageKey');
+
+class HomePage extends StatefulWidget {
+ const HomePage({Key key, this.title}) : super(key: key);
+
+ final String title;
+
+ @override
+ _HomePageState createState() => _HomePageState();
+}
+
+class _HomePageState extends State<HomePage> {
+ int _counter = 0;
+
+ void _incrementCounter() {
+ setState(() {
+ _counter++;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(widget.title),
+ actions: <Widget>[
+ IconButton(
+ key: aboutPageKey,
+ icon: const Icon(Icons.alternate_email),
+ onPressed: () => Navigator.of(context).pushNamed('about'),
+ ),
+ ],
+ ),
+ body: Center(
+ child: ListView.builder(
+ itemExtent: 80,
+ itemBuilder: (BuildContext context, int index) {
+ if (index == 0) {
+ return Column(
+ key: textKey,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ const Text('You have pushed the button this many times:'),
+ Text(
+ '$_counter',
+ style: Theme.of(context).textTheme.headline4,
+ ),
+ ],
+ );
+ } else {
+ return SizedBox(
+ height: 80,
+ child: Padding(
+ padding: const EdgeInsets.all(12),
+ child: Card(
+ elevation: 8,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: <Widget>[
+ Text(
+ 'Line $index',
+ style: Theme.of(context).textTheme.headline5,
+ ),
+ Expanded(child: Container()),
+ const Icon(Icons.camera),
+ const Icon(Icons.face),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+ },
+ ),
+ ),
+ floatingActionButton: FloatingActionButton(
+ onPressed: _incrementCounter,
+ tooltip: 'Increment',
+ child: const Icon(Icons.add),
+ ),
+ );
+ }
+}
diff --git a/packages/web_benchmarks/testing/test_app/lib/main.dart b/packages/web_benchmarks/testing/test_app/lib/main.dart
new file mode 100644
index 0000000..ad775c9
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/lib/main.dart
@@ -0,0 +1,32 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+import 'aboutpage.dart';
+import 'homepage.dart';
+
+void main() {
+ runApp(const MyApp());
+}
+
+class MyApp extends StatelessWidget {
+ const MyApp({Key key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'Flutter Demo',
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
+ visualDensity: VisualDensity.adaptivePlatformDensity,
+ ),
+ initialRoute: 'home',
+ routes: <String, WidgetBuilder>{
+ 'home': (_) => const HomePage(title: 'Flutter Demo Home Page'),
+ 'about': (_) => const AboutPage(),
+ },
+ );
+ }
+}
diff --git a/packages/web_benchmarks/testing/test_app/pubspec.yaml b/packages/web_benchmarks/testing/test_app/pubspec.yaml
new file mode 100644
index 0000000..4d9a9f6
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/pubspec.yaml
@@ -0,0 +1,23 @@
+name: test_app
+description: An example app for web benchmarks testing.
+
+publish_to: 'none'
+
+version: 1.0.0+1
+
+environment:
+ sdk: ">=2.7.0 <3.0.0"
+
+dependencies:
+ cupertino_icons: ^0.1.3
+ flutter:
+ sdk: flutter
+ web_benchmarks:
+ path: ../../
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+flutter:
+ uses-material-design: true
diff --git a/packages/web_benchmarks/testing/test_app/web/favicon.png b/packages/web_benchmarks/testing/test_app/web/favicon.png
new file mode 100644
index 0000000..8aaa46a
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/web/favicon.png
Binary files differ
diff --git a/packages/web_benchmarks/testing/test_app/web/icons/Icon-192.png b/packages/web_benchmarks/testing/test_app/web/icons/Icon-192.png
new file mode 100644
index 0000000..b749bfe
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/web/icons/Icon-192.png
Binary files differ
diff --git a/packages/web_benchmarks/testing/test_app/web/icons/Icon-512.png b/packages/web_benchmarks/testing/test_app/web/icons/Icon-512.png
new file mode 100644
index 0000000..88cfd48
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/web/icons/Icon-512.png
Binary files differ
diff --git a/packages/web_benchmarks/testing/test_app/web/index.html b/packages/web_benchmarks/testing/test_app/web/index.html
new file mode 100644
index 0000000..c52c352
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/web/index.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <meta content="IE=Edge" http-equiv="X-UA-Compatible">
+ <meta name="description" content="A new Flutter project.">
+
+ <!-- iOS meta tags & icons -->
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="apple-mobile-web-app-status-bar-style" content="black">
+ <meta name="apple-mobile-web-app-title" content="test_app">
+ <link rel="apple-touch-icon" href="icons/Icon-192.png">
+
+ <!-- Favicon -->
+ <link rel="icon" type="image/png" href="favicon.png"/>
+
+ <title>test_app</title>
+ <link rel="manifest" href="manifest.json">
+</head>
+<body>
+ <!-- This script installs service_worker.js to provide PWA functionality to
+ application. For more information, see:
+ https://developers.google.com/web/fundamentals/primers/service-workers -->
+ <script>
+ if ('serviceWorker' in navigator) {
+ window.addEventListener('load', function () {
+ navigator.serviceWorker.register('flutter_service_worker.js');
+ });
+ }
+ </script>
+ <script src="main.dart.js" type="application/javascript"></script>
+</body>
+</html>
diff --git a/packages/web_benchmarks/testing/test_app/web/manifest.json b/packages/web_benchmarks/testing/test_app/web/manifest.json
new file mode 100644
index 0000000..13a23690
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/web/manifest.json
@@ -0,0 +1,23 @@
+{
+ "name": "test_app",
+ "short_name": "test_app",
+ "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/web_benchmarks/testing/web_benchmarks_test.dart b/packages/web_benchmarks/testing/web_benchmarks_test.dart
new file mode 100644
index 0000000..2b88121
--- /dev/null
+++ b/packages/web_benchmarks/testing/web_benchmarks_test.dart
@@ -0,0 +1,51 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert' show JsonEncoder;
+import 'dart:io';
+
+import 'package:test/test.dart';
+
+import 'package:web_benchmarks/server.dart';
+
+Future<void> main() async {
+ test('Can run a web benchmark', () async {
+ final BenchmarkResults taskResult = await serveWebBenchmark(
+ benchmarkAppDirectory: Directory('testing/test_app'),
+ entryPoint: 'lib/benchmarks/runner.dart',
+ useCanvasKit: false,
+ );
+
+ for (final String benchmarkName in <String>['scroll', 'page', 'tap']) {
+ for (final String metricName in <String>[
+ 'preroll_frame',
+ 'apply_frame',
+ 'drawFrameDuration',
+ ]) {
+ for (final String valueName in <String>[
+ 'average',
+ 'outlierAverage',
+ 'outlierRatio',
+ 'noise',
+ ]) {
+ expect(
+ taskResult.scores[benchmarkName].where((BenchmarkScore score) =>
+ score.metric == '$metricName.$valueName'),
+ hasLength(1),
+ );
+ }
+ }
+ expect(
+ taskResult.scores[benchmarkName].where(
+ (BenchmarkScore score) => score.metric == 'totalUiFrame.average'),
+ hasLength(1),
+ );
+ }
+
+ expect(
+ const JsonEncoder.withIndent(' ').convert(taskResult.toJson()),
+ isA<String>(),
+ );
+ }, timeout: Timeout.none);
+}
diff --git a/packages/xdg_directories/.gitignore b/packages/xdg_directories/.gitignore
new file mode 100644
index 0000000..bb431f0
--- /dev/null
+++ b/packages/xdg_directories/.gitignore
@@ -0,0 +1,75 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+build/
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Flutter.podspec
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/xdg_directories/.metadata b/packages/xdg_directories/.metadata
new file mode 100644
index 0000000..990be38
--- /dev/null
+++ b/packages/xdg_directories/.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: d8c0deb1b6c116be79ceeca005f893be34ab5df2
+ channel: master
+
+project_type: package
diff --git a/packages/xdg_directories/CHANGELOG.md b/packages/xdg_directories/CHANGELOG.md
new file mode 100644
index 0000000..1a525f0
--- /dev/null
+++ b/packages/xdg_directories/CHANGELOG.md
@@ -0,0 +1,15 @@
+## 0.2.0
+
+* Migrated to null safety.
+
+## 0.1.2
+
+* Broaden dependencies to allow nullsafety version of process, meta, and path to be OK.
+
+## 0.1.1
+
+* Remove flutter, flutter_test from pubspec dependencies.
+
+## 0.1.0
+
+* Initial release includes all the features described in the README.md
diff --git a/packages/xdg_directories/LICENSE b/packages/xdg_directories/LICENSE
new file mode 100644
index 0000000..4be0666
--- /dev/null
+++ b/packages/xdg_directories/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2020 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google 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/xdg_directories/README.md b/packages/xdg_directories/README.md
new file mode 100644
index 0000000..d7fa16a
--- /dev/null
+++ b/packages/xdg_directories/README.md
@@ -0,0 +1,50 @@
+# `xdg_directories`
+
+A Dart package for reading XDG directory configuration information on Linux.
+
+## Getting Started
+
+On Linux, `xdg` is a system developed by [freedesktop.org](freedesktop.org), a
+project to work on interoperability and shared base technology for free software
+desktop environments for Linux.
+
+This Dart package can be used to determine the directory configuration
+information defined by `xdg`, such as where the Documents or Desktop directories
+are. These are called "user directories" and are defined in configuration file
+in the user's home directory.
+
+See [this wiki](https://wiki.archlinux.org/index.php/XDG_Base_Directory) for
+more details of the XDG Base Directory implementation.
+
+To use this package, the basic XDG values for the following are available via a Dart API:
+
+ - `dataHome` - The single base directory relative to which user-specific data
+ files should be written. (Corresponds to `$XDG_DATA_HOME`).
+
+ - `configHome` - The a single base directory relative to which user-specific
+ configuration files should be written. (Corresponds to `$XDG_CONFIG_HOME`).
+
+ - `dataDirs` - The list of preference-ordered base directories relative to
+ which data files should be searched. (Corresponds to `$XDG_DATA_DIRS`).
+
+ - `configDirs` - The list of preference-ordered base directories relative to
+ which configuration files should be searched. (Corresponds to
+ `$XDG_CONFIG_DIRS`).
+
+ - `cacheHome` - The base directory relative to which user-specific
+ non-essential (cached) data should be written. (Corresponds to
+ `$XDG_CACHE_HOME`).
+
+ - `runtimeDir` - The base directory relative to which user-specific runtime
+ files and other file objects should be placed. (Corresponds to
+ `$XDG_RUNTIME_DIR`).
+
+ - `getUserDirectoryNames()` - Returns a set of the names of user directories
+ defined in the `xdg` configuration files.
+
+ - `getUserDirectory(String dirName)` - Gets the value of the user dir with the
+ given name. Requesting a user dir that doesn't exist returns `null`. The
+ `dirName` argument is case-insensitive. See [this
+ wiki](https://wiki.archlinux.org/index.php/XDG_user_directories) for more
+ details and what values of `dirName` might be available.
+
diff --git a/packages/xdg_directories/lib/xdg_directories.dart b/packages/xdg_directories/lib/xdg_directories.dart
new file mode 100644
index 0000000..fc5d515
--- /dev/null
+++ b/packages/xdg_directories/lib/xdg_directories.dart
@@ -0,0 +1,188 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+library xdg_directories;
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as path;
+import 'package:process/process.dart';
+
+/// An override function used by the tests to override the environment variable
+/// lookups using [xdgEnvironmentOverride].
+typedef EnvironmentAccessor = String? Function(String envVar);
+
+/// A testing setter that replaces the real environment lookups with an override.
+///
+/// Set to null to stop overriding.
+///
+/// Only available to tests.
+@visibleForTesting
+set xdgEnvironmentOverride(EnvironmentAccessor? override) {
+ _xdgEnvironmentOverride = override;
+ _getenv = _xdgEnvironmentOverride ?? _productionGetEnv;
+}
+
+/// A testing getter that returns the current value of the override that
+/// replaces the real environment lookups with an override.
+///
+/// Only available to tests.
+@visibleForTesting
+EnvironmentAccessor? get xdgEnvironmentOverride => _xdgEnvironmentOverride;
+EnvironmentAccessor? _xdgEnvironmentOverride;
+EnvironmentAccessor _getenv = _productionGetEnv;
+String? _productionGetEnv(String value) => Platform.environment[value];
+
+/// A testing function that replaces the process manager used to run xdg-user-path
+/// with the one supplied.
+///
+/// Only available to tests.
+@visibleForTesting
+set xdgProcessManager(ProcessManager processManager) {
+ _processManager = processManager;
+}
+
+ProcessManager _processManager = const LocalProcessManager();
+
+List<Directory> _directoryListFromEnvironment(
+ String envVar, List<Directory> fallback) {
+ ArgumentError.checkNotNull(envVar);
+ ArgumentError.checkNotNull(fallback);
+ final String? value = _getenv(envVar);
+ if (value == null || value.isEmpty) {
+ return fallback;
+ }
+ return value.split(':').where((String value) {
+ return value.isNotEmpty;
+ }).map<Directory>((String entry) {
+ return Directory(entry);
+ }).toList();
+}
+
+Directory? _directoryFromEnvironment(String envVar) {
+ ArgumentError.checkNotNull(envVar);
+ final String? value = _getenv(envVar);
+ if (value == null || value.isEmpty) {
+ return null;
+ }
+ return Directory(value);
+}
+
+Directory _directoryFromEnvironmentWithFallback(
+ String envVar, String fallback) {
+ ArgumentError.checkNotNull(envVar);
+ final String? value = _getenv(envVar);
+ if (value == null || value.isEmpty) {
+ return _getDirectory(fallback);
+ }
+ return Directory(value);
+}
+
+// Creates a Directory from a fallback path.
+Directory _getDirectory(String subdir) {
+ ArgumentError.checkNotNull(subdir);
+ assert(subdir.isNotEmpty);
+ final String? homeDir = _getenv('HOME');
+ if (homeDir == null || homeDir.isEmpty) {
+ throw StateError(
+ 'The "HOME" environment variable is not set. This package (and POSIX) '
+ 'requires that HOME be set.');
+ }
+ return Directory(path.joinAll(<String>[homeDir, subdir]));
+}
+
+/// The base directory relative to which user-specific
+/// non-essential (cached) data should be written. (Corresponds to
+/// `$XDG_CACHE_HOME`).
+///
+/// Throws [StateError] if the HOME environment variable is not set.
+Directory get cacheHome =>
+ _directoryFromEnvironmentWithFallback('XDG_CACHE_HOME', '.cache');
+
+/// The list of preference-ordered base directories relative to
+/// which configuration files should be searched. (Corresponds to
+/// `$XDG_CONFIG_DIRS`).
+///
+/// Throws [StateError] if the HOME environment variable is not set.
+List<Directory> get configDirs {
+ return _directoryListFromEnvironment(
+ 'XDG_CONFIG_DIRS',
+ <Directory>[Directory('/etc/xdg')],
+ );
+}
+
+/// The a single base directory relative to which user-specific
+/// configuration files should be written. (Corresponds to `$XDG_CONFIG_HOME`).
+///
+/// Throws [StateError] if the HOME environment variable is not set.
+Directory get configHome =>
+ _directoryFromEnvironmentWithFallback('XDG_CONFIG_HOME', '.config');
+
+/// The list of preference-ordered base directories relative to
+/// which data files should be searched. (Corresponds to `$XDG_DATA_DIRS`).
+///
+/// Throws [StateError] if the HOME environment variable is not set.
+List<Directory> get dataDirs {
+ return _directoryListFromEnvironment(
+ 'XDG_DATA_DIRS',
+ <Directory>[Directory('/usr/local/share'), Directory('/usr/share')],
+ );
+}
+
+/// The base directory relative to which user-specific data files should be
+/// written. (Corresponds to `$XDG_DATA_HOME`).
+///
+/// Throws [StateError] if the HOME environment variable is not set.
+Directory get dataHome =>
+ _directoryFromEnvironmentWithFallback('XDG_DATA_HOME', '.local/share');
+
+/// The base directory relative to which user-specific runtime
+/// files and other file objects should be placed. (Corresponds to
+/// `$XDG_RUNTIME_DIR`).
+///
+/// Throws [StateError] if the HOME environment variable is not set.
+Directory? get runtimeDir => _directoryFromEnvironment('XDG_RUNTIME_DIR');
+
+/// Gets the xdg user directory named by `dirName`.
+///
+/// Use [getUserDirectoryNames] to find out the list of available names.
+Directory? getUserDirectory(String dirName) {
+ final ProcessResult result = _processManager.runSync(
+ <String>['xdg-user-dir', dirName],
+ includeParentEnvironment: true,
+ stdoutEncoding: utf8,
+ );
+ final String path = (result.stdout as String).split('\n')[0];
+ return Directory(path);
+}
+
+/// Gets the set of user directory names that xdg knows about.
+///
+/// These are not paths, they are names of xdg values. Call [getUserDirectory]
+/// to get the associated directory.
+///
+/// These are the names of the variables in "[configHome]/user-dirs.dirs", with
+/// the `XDG_` prefix removed and the `_DIR` suffix removed.
+Set<String> getUserDirectoryNames() {
+ final File configFile = File(path.join(configHome.path, 'user-dirs.dirs'));
+ List<String> contents;
+ try {
+ contents = configFile.readAsLinesSync();
+ } on FileSystemException {
+ return const <String>{};
+ }
+ final Set<String> result = <String>{};
+ final RegExp dirRegExp =
+ RegExp(r'^\s*XDG_(?<dirname>[^=]*)_DIR\s*=\s*(?<dir>.*)\s*$');
+ for (final String line in contents) {
+ final RegExpMatch? match = dirRegExp.firstMatch(line);
+ if (match == null) {
+ continue;
+ }
+ result.add(match.namedGroup('dirname')!);
+ }
+ return result;
+}
diff --git a/packages/xdg_directories/pubspec.yaml b/packages/xdg_directories/pubspec.yaml
new file mode 100644
index 0000000..197813e
--- /dev/null
+++ b/packages/xdg_directories/pubspec.yaml
@@ -0,0 +1,15 @@
+name: xdg_directories
+description: A Dart package for reading XDG directory configuration information on Linux.
+version: 0.2.0
+homepage: https://github.com/flutter/packages/tree/master/packages/xdg_directories
+
+environment:
+ sdk: ">=2.12.0-0 <3.0.0"
+
+dependencies:
+ meta: ^1.3.0
+ path: ^1.8.0
+ process: ^4.0.0
+
+dev_dependencies:
+ test: ^1.16.0
diff --git a/packages/xdg_directories/test/xdg_directories_test.dart b/packages/xdg_directories/test/xdg_directories_test.dart
new file mode 100644
index 0000000..9bf5c0f
--- /dev/null
+++ b/packages/xdg_directories/test/xdg_directories_test.dart
@@ -0,0 +1,133 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:test/fake.dart';
+import 'package:test/test.dart';
+import 'package:path/path.dart' as path;
+import 'package:process/process.dart';
+
+import 'package:xdg_directories/xdg_directories.dart' as xdg;
+
+void main() {
+ final Map<String, String> fakeEnv = <String, String>{};
+ late Directory tmpDir;
+
+ String testPath(String subdir) => path.join(tmpDir.path, subdir);
+
+ setUp(() {
+ tmpDir = Directory.systemTemp.createTempSync('xdg_test');
+ fakeEnv.clear();
+ fakeEnv['HOME'] = tmpDir.path;
+ fakeEnv['XDG_CACHE_HOME'] = testPath('.test_cache');
+ fakeEnv['XDG_CONFIG_DIRS'] = testPath('etc/test_xdg');
+ fakeEnv['XDG_CONFIG_HOME'] = testPath('.test_config');
+ fakeEnv['XDG_DATA_DIRS'] =
+ '${testPath('usr/local/test_share')}:${testPath('usr/test_share')}';
+ fakeEnv['XDG_DATA_HOME'] = testPath('.local/test_share');
+ fakeEnv['XDG_RUNTIME_DIR'] = testPath('.local/test_runtime');
+ Directory(fakeEnv['XDG_CONFIG_HOME']!).createSync(recursive: true);
+ Directory(fakeEnv['XDG_CACHE_HOME']!).createSync(recursive: true);
+ Directory(fakeEnv['XDG_DATA_HOME']!).createSync(recursive: true);
+ Directory(fakeEnv['XDG_RUNTIME_DIR']!).createSync(recursive: true);
+ File(path.join(fakeEnv['XDG_CONFIG_HOME']!, 'user-dirs.dirs'))
+ .writeAsStringSync(r'''
+XDG_DESKTOP_DIR="$HOME/Desktop"
+XDG_DOCUMENTS_DIR="$HOME/Documents"
+XDG_DOWNLOAD_DIR="$HOME/Downloads"
+XDG_MUSIC_DIR="$HOME/Music"
+XDG_PICTURES_DIR="$HOME/Pictures"
+XDG_PUBLICSHARE_DIR="$HOME/Public"
+XDG_TEMPLATES_DIR="$HOME/Templates"
+XDG_VIDEOS_DIR="$HOME/Videos"
+''');
+ xdg.xdgEnvironmentOverride = (String key) => fakeEnv[key];
+ });
+
+ tearDown(() {
+ tmpDir.deleteSync(recursive: true);
+ // Stop overriding the environment accessor.
+ xdg.xdgEnvironmentOverride = null;
+ });
+ void expectDirList(List<Directory> values, List<String> expected) {
+ final List<String> valueStr =
+ values.map<String>((Directory directory) => directory.path).toList();
+ expect(valueStr, orderedEquals(expected));
+ }
+
+ test('Default fallback values work', () {
+ fakeEnv.clear();
+ fakeEnv['HOME'] = tmpDir.path;
+ expect(xdg.cacheHome.path, equals(testPath('.cache')));
+ expect(xdg.configHome.path, equals(testPath('.config')));
+ expect(xdg.dataHome.path, equals(testPath('.local/share')));
+ expect(xdg.runtimeDir, isNull);
+
+ expectDirList(xdg.configDirs, <String>['/etc/xdg']);
+ expectDirList(xdg.dataDirs, <String>['/usr/local/share', '/usr/share']);
+ });
+
+ test('Values pull from environment', () {
+ expect(xdg.cacheHome.path, equals(testPath('.test_cache')));
+ expect(xdg.configHome.path, equals(testPath('.test_config')));
+ expect(xdg.dataHome.path, equals(testPath('.local/test_share')));
+ expect(xdg.runtimeDir, isNotNull);
+ expect(xdg.runtimeDir!.path, equals(testPath('.local/test_runtime')));
+
+ expectDirList(xdg.configDirs, <String>[testPath('etc/test_xdg')]);
+ expectDirList(xdg.dataDirs, <String>[
+ testPath('usr/local/test_share'),
+ testPath('usr/test_share'),
+ ]);
+ });
+
+ test('Can get userDirs', () {
+ final Map<String, String> expected = <String, String>{
+ 'DESKTOP': testPath('Desktop'),
+ 'DOCUMENTS': testPath('Documents'),
+ 'DOWNLOAD': testPath('Downloads'),
+ 'MUSIC': testPath('Music'),
+ 'PICTURES': testPath('Pictures'),
+ 'PUBLICSHARE': testPath('Public'),
+ 'TEMPLATES': testPath('Templates'),
+ 'VIDEOS': testPath('Videos'),
+ };
+ xdg.xdgProcessManager = FakeProcessManager(expected);
+ final Set<String> userDirs = xdg.getUserDirectoryNames();
+ expect(userDirs, equals(expected.keys.toSet()));
+ for (final String key in userDirs) {
+ expect(xdg.getUserDirectory(key)!.path, equals(expected[key]),
+ reason: 'Path $key value not correct');
+ }
+ xdg.xdgProcessManager = const LocalProcessManager();
+ });
+
+ test('Throws StateError when HOME not set', () {
+ fakeEnv.clear();
+ expect(() {
+ xdg.configHome;
+ }, throwsStateError);
+ });
+}
+
+class FakeProcessManager extends Fake implements ProcessManager {
+ FakeProcessManager(this.expected);
+
+ Map<String, String> expected;
+
+ @override
+ ProcessResult runSync(
+ List<dynamic> command, {
+ String? workingDirectory,
+ Map<String, String>? environment,
+ bool includeParentEnvironment = true,
+ bool runInShell = false,
+ Encoding stdoutEncoding = systemEncoding,
+ Encoding stderrEncoding = systemEncoding,
+ }) {
+ return ProcessResult(0, 0, expected[command[1]]!, '');
+ }
+}
diff --git a/script/check_publish.sh b/script/check_publish.sh
new file mode 100755
index 0000000..40e0dcc
--- /dev/null
+++ b/script/check_publish.sh
@@ -0,0 +1,39 @@
+#!/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.
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
+REPO_DIR="$(dirname "$SCRIPT_DIR")"
+
+source "$SCRIPT_DIR/common.sh"
+
+function check_publish() {
+ local failures=()
+ for package_name in "$@"; do
+ local dir="$REPO_DIR/packages/$package_name"
+ echo "Checking that $package_name can be published."
+ if (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 "WARNING: The following ${#failures[@]} package(s) failed the publishing check:"
+ for failure in "${failures[@]}"; do
+ error "$failure"
+ done
+ fi
+ return 0
+}
+
+# Sets CHANGED_PACKAGE_LIST
+check_changed_packages
+
+if [[ "${#CHANGED_PACKAGE_LIST[@]}" != 0 ]]; then
+ check_publish "${CHANGED_PACKAGE_LIST[@]}"
+fi
diff --git a/script/common.sh b/script/common.sh
new file mode 100644
index 0000000..eaf94c7
--- /dev/null
+++ b/script/common.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+function error() {
+ echo "$@" 1>&2
+}
+
+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="$(git merge-base --fork-point FETCH_HEAD HEAD || git merge-base FETCH_HEAD HEAD)"
+ if [[ "$?" == 0 ]]; 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
+
+ # Filter out any packages that don't have a pubspec.yaml: they have probably
+ # been deleted in this PR.
+ CHANGED_PACKAGES=""
+ CHANGED_PACKAGE_LIST=()
+ for package in "${packages[@]}"; do
+ if [[ -f "$REPO_DIR/packages/$package/pubspec.yaml" ]]; 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/incremental_build.sh b/script/incremental_build.sh
new file mode 100755
index 0000000..283c9a5
--- /dev/null
+++ b/script/incremental_build.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
+REPO_DIR="$(dirname "$SCRIPT_DIR")"
+
+source "$SCRIPT_DIR/common.sh"
+
+# Set some default actions if run without arguments.
+ACTIONS=("$@")
+if [[ "${#ACTIONS[@]}" == 0 ]]; then
+ ACTIONS=("test" "analyze" "java-test")
+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[@]}" $BUILD_SHARDING)
+else
+ # Sets CHANGED_PACKAGES
+ check_changed_packages
+
+ if [[ "$CHANGED_PACKAGES" == "" ]]; then
+ echo "Running for all packages"
+ (cd "$REPO_DIR" && pub global run flutter_plugin_tools "${ACTIONS[@]}" $BUILD_SHARDING)
+ else
+ (cd "$REPO_DIR" && pub global run flutter_plugin_tools "${ACTIONS[@]}" --plugins="$CHANGED_PACKAGES" $BUILD_SHARDING)
+ fi
+fi
diff --git a/script/install_chromium.sh b/script/install_chromium.sh
new file mode 100755
index 0000000..0ad8682
--- /dev/null
+++ b/script/install_chromium.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+set -e
+set -x
+
+# The build of Chromium used to test web functionality.
+#
+# Chromium builds can be located here: https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/
+CHROMIUM_BUILD=768968
+
+mkdir .chromium
+wget "https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F${CHROMIUM_BUILD}%2Fchrome-linux.zip?alt=media" -O .chromium/chromium.zip
+unzip .chromium/chromium.zip -d .chromium/
+export CHROME_EXECUTABLE=$(pwd)/.chromium/chrome-linux/chrome
+echo $CHROME_EXECUTABLE
+$CHROME_EXECUTABLE --version
diff --git a/script/local_tests.sh b/script/local_tests.sh
new file mode 100755
index 0000000..ca12e4d
--- /dev/null
+++ b/script/local_tests.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
+REPO_DIR="$(dirname "$SCRIPT_DIR")"
+TEST_SCRIPT_NAME="run_tests.sh"
+
+source "$SCRIPT_DIR/common.sh"
+
+check_changed_packages
+
+for PACKAGE in $CHANGED_PACKAGE_LIST; do
+ PACKAGE_PATH=./packages/$PACKAGE
+ TEST_SCRIPT=$PACKAGE_PATH/$TEST_SCRIPT_NAME
+ if [ -e $TEST_SCRIPT ]; then
+ pushd $PWD
+ cd $PACKAGE_PATH
+ ls
+ ./$TEST_SCRIPT_NAME
+ popd
+ fi
+done
diff --git a/third_party/packages/bsdiff/CHANGELOG.md b/third_party/packages/bsdiff/CHANGELOG.md
new file mode 100644
index 0000000..5703fc8
--- /dev/null
+++ b/third_party/packages/bsdiff/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.1.0
+
+* Initial Open Source release.
diff --git a/third_party/packages/bsdiff/LICENSE b/third_party/packages/bsdiff/LICENSE
new file mode 100644
index 0000000..d04a529
--- /dev/null
+++ b/third_party/packages/bsdiff/LICENSE
@@ -0,0 +1,23 @@
+Copyright 2003-2005 Colin Percival. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. 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.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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/third_party/packages/bsdiff/README.md b/third_party/packages/bsdiff/README.md
new file mode 100644
index 0000000..18752b4
--- /dev/null
+++ b/third_party/packages/bsdiff/README.md
@@ -0,0 +1,29 @@
+# BSDiff
+
+[](
+https://pub.dartlang.org/packages/bsdiff)
+
+Binary diff/patch algorithm based on
+[bsdiff 4.3](http://www.daemonology.net/bsdiff/) by Colin Percival.
+It's very effective at compressesing incremental changes in binaries.
+
+This implementation has the following differences from Colin's code,
+to make it easier to read and apply patches in Java and Objective C:
+
+* Using gzip instead of bzip2 because gzip is included in JDK by default
+* Using big- instead of little-endian serialization to simplify Java code
+* Using two's complement instead of high-bit coding for negatives numbers
+
+## Usage
+To use this package, add `bsdiff` as a
+[dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/).
+
+## Example
+
+Import the library via
+``` dart
+import 'package:bsdiff/bsdiff.dart';
+```
+
+Then use the `bsdiff` and `bspatch` Dart functions in your code. To see how this is done,
+check out the [example app](example/main.dart).
diff --git a/third_party/packages/bsdiff/example/main.dart b/third_party/packages/bsdiff/example/main.dart
new file mode 100644
index 0000000..5e2eca7
--- /dev/null
+++ b/third_party/packages/bsdiff/example/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.
+
+// Example script to illustrate how to use the bsdiff package to generate and apply patches.
+
+import 'dart:typed_data';
+
+import 'package:bsdiff/bsdiff.dart';
+
+void main() {
+ final Uint8List originalData =
+ Uint8List.fromList(List<int>.generate(1000, (int index) => index));
+ final Uint8List modifiedData =
+ Uint8List.fromList(List<int>.generate(2000, (int index) => 2 * index));
+
+ print('Original data size ${originalData.length} bytes');
+ print('Modified data size ${modifiedData.length} bytes');
+
+ final Uint8List generatedPatch = bsdiff(originalData, modifiedData);
+ final Uint8List restoredData = bspatch(originalData, generatedPatch);
+
+ print('Generated patch is ${generatedPatch.length} bytes');
+ print('Restored data size ${restoredData.length} bytes');
+}
diff --git a/third_party/packages/bsdiff/example/pubspec.yaml b/third_party/packages/bsdiff/example/pubspec.yaml
new file mode 100644
index 0000000..d5c9099
--- /dev/null
+++ b/third_party/packages/bsdiff/example/pubspec.yaml
@@ -0,0 +1,10 @@
+name: main
+description: A simple example of how to use bsdiff to generate and apply patches.
+version: 0.1.0
+
+dependencies:
+ bsdiff:
+ path: ..
+
+environment:
+ sdk: ">=2.1.1-dev.2.0 <3.0.0"
diff --git a/third_party/packages/bsdiff/lib/bsdiff.dart b/third_party/packages/bsdiff/lib/bsdiff.dart
new file mode 100644
index 0000000..7af6acc
--- /dev/null
+++ b/third_party/packages/bsdiff/lib/bsdiff.dart
@@ -0,0 +1,412 @@
+// Copyright 2003-2005 Colin Percival. All 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:math';
+import 'dart:typed_data';
+
+void _split(List<int> idata, List<int> vdata, int start, int len, int h) {
+ if (len < 16) {
+ for (int j, k = start; k < start + len; k += j) {
+ j = 1;
+ int x = vdata[idata[k] + h];
+ for (int i = 1; k + i < start + len; i++) {
+ if (vdata[idata[k + i] + h] < x) {
+ x = vdata[idata[k + i] + h];
+ j = 0;
+ }
+ if (vdata[idata[k + i] + h] == x) {
+ final int tmp = idata[k + j];
+ idata[k + j] = idata[k + i];
+ idata[k + i] = tmp;
+ j++;
+ }
+ }
+ for (int i = 0; i < j; i++) {
+ vdata[idata[k + i]] = k + j - 1;
+ }
+ if (j == 1) {
+ idata[k] = -1;
+ }
+ }
+ return;
+ }
+
+ final int x = vdata[idata[start + len ~/ 2] + h];
+ int jj = 0;
+ int kk = 0;
+ for (int i = start; i < start + len; i++) {
+ if (vdata[idata[i] + h] < x) {
+ jj++;
+ }
+ if (vdata[idata[i] + h] == x) {
+ kk++;
+ }
+ }
+ jj += start;
+ kk += jj;
+
+ int i = start;
+ int j = 0;
+ int k = 0;
+ while (i < jj) {
+ if (vdata[idata[i] + h] < x) {
+ i++;
+ } else if (vdata[idata[i] + h] == x) {
+ final int tmp = idata[i];
+ idata[i] = idata[jj + j];
+ idata[jj + j] = tmp;
+ j++;
+ } else {
+ final int tmp = idata[i];
+ idata[i] = idata[kk + k];
+ idata[kk + k] = tmp;
+ k++;
+ }
+ }
+
+ while (jj + j < kk) {
+ if (vdata[idata[jj + j] + h] == x) {
+ j++;
+ } else {
+ final int tmp = idata[jj + j];
+ idata[jj + j] = idata[kk + k];
+ idata[kk + k] = tmp;
+ k++;
+ }
+ }
+
+ if (jj > start) {
+ _split(idata, vdata, start, jj - start, h);
+ }
+
+ for (i = 0; i < kk - jj; i++) {
+ vdata[idata[jj + i]] = kk - 1;
+ }
+ if (jj == kk - 1) {
+ idata[jj] = -1;
+ }
+
+ if (start + len > kk) {
+ _split(idata, vdata, kk, start + len - kk, h);
+ }
+}
+
+void _qsufsort(List<int> idata, List<int> vdata, Uint8List olddata) {
+ final int oldsize = olddata.length;
+ final List<int> buckets = List<int>(256);
+
+ for (int i = 0; i < 256; i++) {
+ buckets[i] = 0;
+ }
+ for (int i = 0; i < oldsize; i++) {
+ buckets[olddata[i]]++;
+ }
+ for (int i = 1; i < 256; i++) {
+ buckets[i] += buckets[i - 1];
+ }
+
+ for (int i = 255; i > 0; i--) {
+ buckets[i] = buckets[i - 1];
+ }
+ buckets[0] = 0;
+
+ for (int i = 0; i < oldsize; i++) {
+ idata[++buckets[olddata[i]]] = i;
+ }
+ idata[0] = oldsize;
+
+ for (int i = 0; i < oldsize; i++) {
+ vdata[i] = buckets[olddata[i]];
+ }
+ vdata[oldsize] = 0;
+
+ for (int i = 1; i < 256; i++) {
+ if (buckets[i] == buckets[i - 1] + 1) {
+ idata[buckets[i]] = -1;
+ }
+ }
+ idata[0] = -1;
+
+ for (int h = 1; idata[0] != -(oldsize + 1); h += h) {
+ int len = 0;
+ int i;
+ for (i = 0; i < oldsize + 1;) {
+ if (idata[i] < 0) {
+ len -= idata[i];
+ i -= idata[i];
+ } else {
+ if (len != 0) {
+ idata[i - len] = -len;
+ }
+ len = vdata[idata[i]] + 1 - i;
+ _split(idata, vdata, i, len, h);
+ i += len;
+ len = 0;
+ }
+ }
+ if (len != 0) {
+ idata[i - len] = -len;
+ }
+ }
+
+ for (int i = 0; i < oldsize + 1; i++) {
+ idata[vdata[i]] = i;
+ }
+}
+
+int _matchlen(Uint8List olddata, int oldskip, Uint8List newdata, int newskip) {
+ final int n = min(olddata.length - oldskip, newdata.length - newskip);
+ for (int i = 0; i < n; i++) {
+ if (olddata[oldskip + i] != newdata[newskip + i]) {
+ return i;
+ }
+ }
+ return n;
+}
+
+int _memcmp(Uint8List data1, int skip1, Uint8List data2, int skip2) {
+ final int n = min(data1.length - skip1, data2.length - skip2);
+ for (int i = 0; i < n; i++) {
+ if (data1[i + skip1] != data2[i + skip2]) {
+ return data1[i + skip1] < data2[i + skip2] ? -1 : 1;
+ }
+ }
+ return 0;
+}
+
+class _Ref<T> {
+ T value;
+}
+
+int _search(List<int> idata, Uint8List olddata, Uint8List newdata, int newskip,
+ int start, int end, _Ref<int> pos) {
+ if (end - start < 2) {
+ final int x = _matchlen(olddata, idata[start], newdata, newskip);
+ final int y = _matchlen(olddata, idata[end], newdata, newskip);
+
+ if (x > y) {
+ pos.value = idata[start];
+ return x;
+ } else {
+ pos.value = idata[end];
+ return y;
+ }
+ }
+
+ final int x = start + (end - start) ~/ 2;
+ if (_memcmp(olddata, idata[x], newdata, newskip) < 0) {
+ return _search(idata, olddata, newdata, newskip, x, end, pos);
+ } else {
+ return _search(idata, olddata, newdata, newskip, start, x, pos);
+ }
+}
+
+List<int> _int64bytes(int i) =>
+ (ByteData(8)..setInt64(0, i)).buffer.asUint8List();
+
+Uint8List bsdiff(List<int> olddata, List<int> newdata) {
+ final int oldsize = olddata.length;
+ final int newsize = newdata.length;
+
+ final List<int> idata = List<int>(oldsize + 1);
+ _qsufsort(idata, List<int>(oldsize + 1), olddata);
+
+ final Uint8List db = Uint8List(newsize + 1);
+ final Uint8List eb = Uint8List(newsize + 1);
+
+ int dblen = 0;
+ int eblen = 0;
+
+ BytesBuilder buf = BytesBuilder();
+ final _Ref<int> pos = _Ref<int>();
+
+ for (int scan = 0, len = 0, lastscan = 0, lastpos = 0, lastoffset = 0;
+ scan < newsize;) {
+ int oldscore = 0;
+
+ for (int scsc = scan += len; scan < newsize; scan++) {
+ len = _search(idata, olddata, newdata, scan, 0, oldsize, pos);
+
+ for (; scsc < scan + len; scsc++) {
+ if ((scsc + lastoffset < oldsize) &&
+ (olddata[scsc + lastoffset] == newdata[scsc])) {
+ oldscore++;
+ }
+ }
+ if (((len == oldscore) && (len != 0)) || (len > oldscore + 8)) {
+ break;
+ }
+ if ((scan + lastoffset < oldsize) &&
+ (olddata[scan + lastoffset] == newdata[scan])) {
+ oldscore--;
+ }
+ }
+
+ if ((len != oldscore) || (scan == newsize)) {
+ int lenf = 0;
+ int lenb = 0;
+
+ for (int sf = 0, s = 0, i = 0;
+ (lastscan + i < scan) && (lastpos + i < oldsize);) {
+ if (olddata[lastpos + i] == newdata[lastscan + i]) {
+ s++;
+ }
+ i++;
+ if (s * 2 - i > sf * 2 - lenf) {
+ sf = s;
+ lenf = i;
+ }
+ }
+
+ if (scan < newsize) {
+ for (int sb = 0, s = 0, i = 1;
+ (scan >= lastscan + i) && (pos.value >= i);
+ i++) {
+ if (olddata[pos.value - i] == newdata[scan - i]) {
+ s++;
+ }
+ if (s * 2 - i > sb * 2 - lenb) {
+ sb = s;
+ lenb = i;
+ }
+ }
+ }
+
+ if (lastscan + lenf > scan - lenb) {
+ final int overlap = (lastscan + lenf) - (scan - lenb);
+ int lens = 0;
+ for (int ss = 0, s = 0, i = 0; i < overlap; i++) {
+ if (newdata[lastscan + lenf - overlap + i] ==
+ olddata[lastpos + lenf - overlap + i]) {
+ s++;
+ }
+ if (newdata[scan - lenb + i] == olddata[pos.value - lenb + i]) {
+ s--;
+ }
+ if (s > ss) {
+ ss = s;
+ lens = i + 1;
+ }
+ }
+
+ lenf += lens - overlap;
+ lenb -= lens;
+ }
+
+ for (int i = 0; i < lenf; i++) {
+ db[dblen + i] = newdata[lastscan + i] - olddata[lastpos + i];
+ }
+
+ for (int i = 0; i < (scan - lenb) - (lastscan + lenf); i++) {
+ eb[eblen + i] = newdata[lastscan + lenf + i];
+ }
+
+ dblen += lenf;
+ eblen += (scan - lenb) - (lastscan + lenf);
+
+ buf.add(_int64bytes(lenf));
+ buf.add(_int64bytes((scan - lenb) - (lastscan + lenf)));
+ buf.add(_int64bytes((pos.value - lenb) - (lastpos + lenf)));
+
+ lastscan = scan - lenb;
+ lastpos = pos.value - lenb;
+ lastoffset = pos.value - scan;
+ }
+ }
+
+ final BytesBuilder out = BytesBuilder();
+
+ out.add(const AsciiCodec().encode('BZDIFF40').toList());
+ out.add(_int64bytes(0));
+ out.add(_int64bytes(0));
+ out.add(_int64bytes(newsize));
+
+ out.add(gzip.encoder.convert(buf.takeBytes()));
+
+ final int len1 = out.length;
+
+ buf = BytesBuilder();
+ buf.add(db.sublist(0, dblen));
+ out.add(gzip.encoder.convert(buf.takeBytes()));
+
+ final int len2 = out.length;
+
+ buf = BytesBuilder();
+ buf.add(eb.sublist(0, eblen));
+ out.add(gzip.encoder.convert(buf.takeBytes()));
+
+ final Uint8List bytes = Uint8List.fromList(out.takeBytes());
+ final ByteData data = ByteData.view(bytes.buffer);
+ data.setUint64(8, len1 - 32);
+ data.setUint64(16, len2 - len1);
+
+ return bytes;
+}
+
+Uint8List bspatch(List<int> olddata, List<int> diffdata) {
+ final List<int> magic = diffdata.sublist(0, 8);
+ if (const AsciiCodec().decode(magic) != 'BZDIFF40') {
+ throw Exception('Invalid magic');
+ }
+
+ final ByteData header =
+ ByteData.view(Uint8List.fromList(diffdata.sublist(0, 32)).buffer);
+
+ final int ctrllen = header.getInt64(8);
+ final int datalen = header.getInt64(16);
+ final int newsize = header.getInt64(24);
+
+ final List<int> cpf =
+ gzip.decoder.convert(diffdata.sublist(32, 32 + ctrllen));
+ final List<int> dpf = gzip.decoder
+ .convert(diffdata.sublist(32 + ctrllen, 32 + ctrllen + datalen));
+ final List<int> epf = gzip.decoder
+ .convert(diffdata.sublist(32 + ctrllen + datalen, diffdata.length));
+
+ final ByteData cpfdata = ByteData.view(Uint8List.fromList(cpf).buffer);
+
+ final Uint8List newdata = Uint8List(newsize);
+
+ int cpfpos = 0;
+ int dpfpos = 0;
+ int epfpos = 0;
+ int oldpos = 0;
+ int newpos = 0;
+
+ while (newpos < newsize) {
+ final List<int> ctrl = List<int>(3);
+ for (int i = 0; i <= 2; i++) {
+ ctrl[i] = cpfdata.getInt64(8 * cpfpos++);
+ }
+ if (newpos + ctrl[0] > newsize) {
+ throw Exception('Invalid ctrl[0]');
+ }
+
+ newdata.setRange(newpos, newpos + ctrl[0], dpf, dpfpos);
+
+ for (int i = 0; i < ctrl[0]; i++) {
+ if ((oldpos + i >= 0) && (oldpos + i < olddata.length)) {
+ newdata[newpos + i] += olddata[oldpos + i];
+ }
+ }
+
+ dpfpos += ctrl[0];
+ newpos += ctrl[0];
+ oldpos += ctrl[0];
+
+ if (newpos + ctrl[1] > newsize) {
+ throw Exception('Invalid ctrl[0]');
+ }
+
+ newdata.setRange(newpos, newpos + ctrl[1], epf, epfpos);
+
+ epfpos += ctrl[1];
+ newpos += ctrl[1];
+ oldpos += ctrl[2];
+ }
+
+ return newdata;
+}
diff --git a/third_party/packages/bsdiff/pubspec.yaml b/third_party/packages/bsdiff/pubspec.yaml
new file mode 100644
index 0000000..1472d12
--- /dev/null
+++ b/third_party/packages/bsdiff/pubspec.yaml
@@ -0,0 +1,11 @@
+name: bsdiff
+description: Binary diff/patch algorithm based on bsdiff by Colin Percival.
+author: Flutter Team <flutter-dev@googlegroups.com>
+homepage: https://github.com/flutter/packages/tree/master/third_party/packages/bsdiff
+version: 0.1.0
+
+dev_dependencies:
+ test: "^1.3.4"
+
+environment:
+ sdk: ">=2.1.1-dev.2.0 <3.0.0"
diff --git a/third_party/packages/bsdiff/test/bsdiff_test.dart b/third_party/packages/bsdiff/test/bsdiff_test.dart
new file mode 100644
index 0000000..235933a
--- /dev/null
+++ b/third_party/packages/bsdiff/test/bsdiff_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 'dart:typed_data';
+
+import 'package:bsdiff/bsdiff.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('roundtrip', () {
+ final Uint8List a = Uint8List.fromList('Hello'.runes.toList());
+ final Uint8List b = Uint8List.fromList('Hello World'.runes.toList());
+ final Uint8List c = bsdiff(a, b);
+ expect(bspatch(a, c), equals(b));
+ });
+}