diff --git a/.ci/Dockerfile b/.ci/Dockerfile index bae957da08eb..13ac087498d1 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -22,3 +22,8 @@ RUN yes | sdkmanager \ "extras;android;m2repository" RUN yes | sdkmanager --licenses + +# Add repo for Google Chrome and install it +RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - +RUN echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | sudo tee /etc/apt/sources.list.d/google-chrome.list +RUN sudo apt-get update && sudo apt-get install -y --no-install-recommends google-chrome-stable diff --git a/.cirrus.yml b/.cirrus.yml index 6c646976b19f..af37fc630c2f 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -13,7 +13,9 @@ task: activate_script: pub global activate flutter_plugin_tools matrix: - name: publishable - script: ./script/check_publish.sh + script: + - flutter channel stable + - ./script/check_publish.sh - name: format install_script: - wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - @@ -80,7 +82,7 @@ task: task: use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' osx_instance: - image: mojave-xcode-10.2-flutter + image: mojave-xcode-11.2.1-flutter setup_script: - pod repo update upgrade_script: @@ -92,7 +94,7 @@ task: activate_script: pub global activate flutter_plugin_tools create_simulator_script: - xcrun simctl list - - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-12-2 | xargs xcrun simctl boot + - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-2 | xargs xcrun simctl boot matrix: - name: build_all_plugins_ipa script: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e6e3e957af29..f58dfea6a065 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,26 +42,46 @@ USB and debugging enabled on that device. ## Running the tests -Flutter plugins have both unit tests of their Dart API and integration tests that run on a virtual or actual device. +### Integration tests -To run the unit tests: +To run the integration tests using Flutter driver: -``` -flutter test test/_test.dart +```console +cd example +flutter drive test_driver/.dart ``` -To run the integration tests using Flutter driver: +To run integration tests as instrumentation tests on a local Android device: -``` +```console cd example -flutter drive test/.dart +flutter build apk +cd android && ./gradlew -Ptarget=$(pwd)/../test_driver/_test.dart app:connectedAndroidTest ``` -To run integration tests as instrumentation tests on a local Android device: +These tests may also be in folders just named "test," or have filenames ending +with "e2e". + +### Dart unit tests + +To run the unit tests: +```console +flutter test test/_test.dart ``` + +### Java unit tests + +These can be ran through Android Studio once the example app is opened as an +Android project. + +Without Android Studio, they can be ran through the terminal. + +```console cd example -(cd android && ./gradlew -Ptarget=$(pwd)/../test_live/_test.dart connectedAndroidTest) +flutter build apk +cd android +./gradlew test ``` ## Contributing code @@ -101,13 +121,6 @@ Plugins tests are run automatically on contributions using Cirrus CI. However, d cost constraints, pull requests from non-committers may not run all the tests automatically. -The plugins team prefers that unit tests are written using `setMockMethodCallHandler` -rather than using mockito to mock out `MethodChannel`. For a list of the plugins that -are still using the mockito testing style and need to be converted, see -[issue 34284](https://github.com/flutter/flutter/issues/34284). If you are contributing -tests to an existing plugin that uses mockito `MethodChannel`, consider converting -them to use `setMockMethodCallHandler` instead. - Once you've gotten an LGTM from a project maintainer and once your PR has received the green light from all our automated testing, wait for one the package maintainers to merge the pull request and `pub submit` any affected packages. @@ -134,3 +147,74 @@ Newly opened PRs first go through initial triage which results in one of: * **Starting a non trivial review** - if the review requires non trivial effort and the issue is a priority; in this case the maintainer will: * Add the "in review" label to the issue. * Self assign the PR. + +### The release process + +We push releases manually. Generally every merged PR upgrades at least one +plugin's `pubspec.yaml`, so also needs to be published as a package release. The +Flutter team member most involved with the PR should be the person responsible +for publishing the package release. In cases where the PR is authored by a +Flutter maintainer, the publisher should probably be the author. In other cases +where the PR is from a contributor, it's up to the reviewing Flutter team member +to publish the release instead. + +Some things to keep in mind before publishing the release: + +- Has CI ran on the master commit and gone green? Even if CI shows as green on + the PR it's still possible for it to fail on merge, for multiple reasons. + There may have been some bug in the merge that introduced new failures. CI + runs on PRs as it's configured on their branch state, and not on tip of tree. + CI on PRs also only runs tests for packages that it detects have been directly + changed, vs running on every single package on master. +- [Publishing is + forever.](https://dart.dev/tools/pub/publishing#publishing-is-forever) + Hopefully any bugs or breaking in changes in this PR have already been caught + in PR review, but now's a second chance to revert before anything goes live. +- "Don't deploy on a Friday." Consider carefully whether or not it's worth + immediately publishing an update before a stretch of time where you're going + to be unavailable. There may be bugs with the release or questions about it + from people that immediately adopt it, and uncovering and resolving those + support issues will take more time if you're unavailable. + +Releasing a package is a two-step process. + +1. Push the package update to [pub.dev](https://pub.dev) using `pub publish`. +2. Tag the commit with git in the format of `-v`, + and then push the tag to the `flutter/plugins` master branch. This can be + done manually with `git tag $tagname && git push upstream $tagname` while + checked out on the commit that updated `version` in `pubspec.yaml`. + +We've recently updated +[flutter_plugin_tools](https://github.com/flutter/plugin_tools) to wrap both of +those steps into one command to make it a little easier. This new tool is +experimental. Feel free to fall back on manually running `pub publish` and +creating and pushing the tag in git if there are issues with it. + +Install the tool by running: + +```terminal +$ pub global activate flutter_plugin_tools +``` + +Then, from the root of your local `flutter/plugins` repo, use the tool to +publish a release. + +```terminal +$ pub global run flutter_plugin_tools publish-plugin --package $package +``` + +By default the tool tries to push tags to the `upstream` remote, but that and +some additional settings can be configured. Run `pub global activate +flutter_plugin_tools --help` for more usage information. + +The tool wraps `pub publish` for pushing the package to pub, and then will +automatically use git to try and create and push tags. It has some additional +safety checking around `pub publish` too. By default `pub publish` publishes +_everything_, including untracked or uncommitted files in version control. +`flutter_plugin_tools publish-plugin` will first check the status of the local +directory and refuse to publish if there are any mismatched files with version +control present. + +There is a lot about this process that is still to be desired. Some top level +items are being tracked in +[flutter/flutter#27258](https://github.com/flutter/flutter/issues/27258). diff --git a/analysis_options.yaml b/analysis_options.yaml index a73c3a63e1bb..3a657c0e2408 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,140 +1,9 @@ -# 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 -# -# NOTE: Please keep this file in sync with -# https://github.com/flutter/flutter/blob/master/analysis_options.yaml - +include: package:pedantic/analysis_options.yaml analyzer: - language: - enableStrictCallChecks: true - enableSuperMixins: true - enableAssertInitializer: true - 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 exclude: - - 'bin/cache/**' - # the following two are relative to the stocks example and the flutter package respectively - # see https://github.com/dart-lang/sdk/issues/28463 - - 'lib/i18n/stock_messages_*.dart' - - 'lib/src/http/**' - + # Ignore generated files + - '**/*.g.dart' + - 'lib/src/generated/*.dart' linter: rules: - # these rules are documented on and in the same order as - # the Dart Lint rules page to make maintenance easier - # http://dart-lang.github.io/linter/lints/ - - # === error rules === - - avoid_empty_else - - avoid_slow_async_io - - cancel_subscriptions - # - close_sinks # https://github.com/flutter/flutter/issues/5789 - # - comment_references # blocked on https://github.com/dart-lang/dartdoc/issues/1153 - - control_flow_in_finally - - empty_statements - - hash_and_equals - # - invariant_booleans # https://github.com/flutter/flutter/issues/5790 - - iterable_contains_unrelated_type - - list_remove_unrelated_type - # - literal_only_boolean_expressions # https://github.com/flutter/flutter/issues/5791 - - no_adjacent_strings_in_list - - no_duplicate_case_values - - test_types_in_equals - - throw_in_finally - - unrelated_type_equality_checks - - valid_regexps - - # === style rules === - - always_declare_return_types - # - always_put_control_body_on_new_line - - always_require_non_null_named_parameters - - always_specify_types - - annotate_overrides - # - avoid_annotating_with_dynamic # not yet tested - - avoid_as - # - avoid_catches_without_on_clauses # not yet tested - # - avoid_catching_errors # not yet tested - # - avoid_classes_with_only_static_members # not yet tested - # - avoid_function_literals_in_foreach_calls # not yet tested - - avoid_init_to_null - - avoid_null_checks_in_equality_operators - # - avoid_positional_boolean_parameters # not yet tested - - avoid_return_types_on_setters - # - avoid_returning_null # not yet tested - # - avoid_returning_this # not yet tested - # - avoid_setters_without_getters # not yet tested - # - avoid_types_on_closure_parameters # not yet tested - - await_only_futures - - camel_case_types - # - cascade_invocations # not yet tested - # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204 - - directives_ordering - - empty_catches - - empty_constructor_bodies - - implementation_imports - # - join_return_with_assignment # not yet tested - - library_names - - library_prefixes - - non_constant_identifier_names - # - 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_prefixed_library_names - # - parameter_assignments # we do this commonly - - prefer_adjacent_string_concatenation - - prefer_collection_literals - # - prefer_conditional_assignment # not yet tested - - prefer_const_constructors - # - prefer_constructors_over_static_methods # not yet tested - - prefer_contains - - 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 # https://github.com/dart-lang/linter/issues/506 - - prefer_final_locals - # - prefer_foreach # not yet tested - # - prefer_function_declarations_over_variables # not yet tested - - prefer_initializing_formals - # - prefer_interpolation_to_compose_strings # not yet tested - - prefer_is_empty - - prefer_is_not_empty - - prefer_void_to_null - # - recursive_getters # https://github.com/dart-lang/linter/issues/452 - - slash_for_doc_comments - - sort_constructors_first - - sort_unnamed_constructors_first - - super_goes_last - # - type_annotate_public_apis # subset of always_specify_types - - type_init_formals - # - unawaited_futures # https://github.com/flutter/flutter/issues/5793 - - unnecessary_brace_in_string_interps - - unnecessary_const - - unnecessary_getters_setters - # - unnecessary_lambdas # https://github.com/dart-lang/linter/issues/498 - - unnecessary_new - - unnecessary_null_aware_assignments - - unnecessary_null_in_if_null_operators - # - unnecessary_overrides # https://github.com/dart-lang/linter/issues/626 and https://github.com/dart-lang/linter/issues/627 - - unnecessary_statements - - unnecessary_this - - use_rethrow_when_possible - # - use_setters_to_change_properties # not yet tested - # - use_string_buffers # https://github.com/dart-lang/linter/pull/664 - # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review - - # === pub rules === - - package_names + - public_member_api_docs diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md index 1506fd304cf7..1494506fcb73 100644 --- a/packages/android_alarm_manager/CHANGELOG.md +++ b/packages/android_alarm_manager/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.4+3 + +* Add unit tests and DartDocs. + ## 0.4.4+2 * Remove AndroidX warning. diff --git a/packages/android_alarm_manager/example/lib/main.dart b/packages/android_alarm_manager/example/lib/main.dart index e68735a75085..5e20364312bb 100644 --- a/packages/android_alarm_manager/example/lib/main.dart +++ b/packages/android_alarm_manager/example/lib/main.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: public_member_api_docs + import 'dart:async'; import 'package:android_alarm_manager/android_alarm_manager.dart'; diff --git a/packages/android_alarm_manager/lib/android_alarm_manager.dart b/packages/android_alarm_manager/lib/android_alarm_manager.dart index b90823e5c9d4..f31b4bc87525 100644 --- a/packages/android_alarm_manager/lib/android_alarm_manager.dart +++ b/packages/android_alarm_manager/lib/android_alarm_manager.dart @@ -39,8 +39,10 @@ void _alarmManagerCallbackDispatcher() { exit(-1); } + // ignore: inference_failure_on_function_return_type if (closure is Function()) { closure(); + // ignore: inference_failure_on_function_return_type } else if (closure is Function(int)) { final int id = args[1]; closure(id); @@ -52,14 +54,34 @@ void _alarmManagerCallbackDispatcher() { _channel.invokeMethod('AlarmService.initialized'); } +// A lambda that returns the current instant in the form of a [DateTime]. +typedef DateTime _Now(); +// A lambda that gets the handle for the given [callback]. +typedef CallbackHandle _GetCallbackHandle(Function callback); + /// A Flutter plugin for registering Dart callbacks with the Android /// AlarmManager service. /// /// See the example/ directory in this package for sample usage. class AndroidAlarmManager { static const String _channelName = 'plugins.flutter.io/android_alarm_manager'; - static const MethodChannel _channel = - MethodChannel(_channelName, JSONMethodCodec()); + static MethodChannel _channel = + const MethodChannel(_channelName, JSONMethodCodec()); + // Function used to get the current time. It's [DateTime.now] by default. + static _Now _now = () => DateTime.now(); + // Callback used to get the handle for a callback. It's + // [PluginUtilities.getCallbackHandle] by default. + static _GetCallbackHandle _getCallbackHandle = + (Function callback) => PluginUtilities.getCallbackHandle(callback); + + /// This is exposed for the unit tests. It should not be accessed by users of + /// the plugin. + @visibleForTesting + static void setTestOverides( + {_Now now, _GetCallbackHandle getCallbackHandle}) { + _now = (now ?? _now); + _getCallbackHandle = (getCallbackHandle ?? _getCallbackHandle); + } /// Starts the [AndroidAlarmManager] service. This must be called before /// setting any alarms. @@ -68,7 +90,7 @@ class AndroidAlarmManager { /// failure. static Future initialize() async { final CallbackHandle handle = - PluginUtilities.getCallbackHandle(_alarmManagerCallbackDispatcher); + _getCallbackHandle(_alarmManagerCallbackDispatcher); if (handle == null) { return false; } @@ -125,7 +147,7 @@ class AndroidAlarmManager { bool rescheduleOnReboot = false, }) => oneShotAt( - DateTime.now().add(delay), + _now().add(delay), id, callback, alarmClock: alarmClock, @@ -182,10 +204,11 @@ class AndroidAlarmManager { bool wakeup = false, bool rescheduleOnReboot = false, }) async { + // ignore: inference_failure_on_function_return_type assert(callback is Function() || callback is Function(int)); assert(id.bitLength < 32); final int startMillis = time.millisecondsSinceEpoch; - final CallbackHandle handle = PluginUtilities.getCallbackHandle(callback); + final CallbackHandle handle = _getCallbackHandle(callback); if (handle == null) { return false; } @@ -245,13 +268,14 @@ class AndroidAlarmManager { bool wakeup = false, bool rescheduleOnReboot = false, }) async { + // ignore: inference_failure_on_function_return_type assert(callback is Function() || callback is Function(int)); assert(id.bitLength < 32); - final int now = DateTime.now().millisecondsSinceEpoch; + final int now = _now().millisecondsSinceEpoch; final int period = duration.inMilliseconds; final int first = startAt != null ? startAt.millisecondsSinceEpoch : now + period; - final CallbackHandle handle = PluginUtilities.getCallbackHandle(callback); + final CallbackHandle handle = _getCallbackHandle(callback); if (handle == null) { return false; } diff --git a/packages/android_alarm_manager/pubspec.yaml b/packages/android_alarm_manager/pubspec.yaml index 6fe4ed944d55..29a9961b448d 100644 --- a/packages/android_alarm_manager/pubspec.yaml +++ b/packages/android_alarm_manager/pubspec.yaml @@ -1,7 +1,7 @@ name: android_alarm_manager description: Flutter plugin for accessing the Android AlarmManager service, and running Dart code in the background when alarms fire. -version: 0.4.4+2 +version: 0.4.4+3 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager @@ -9,6 +9,10 @@ dependencies: flutter: sdk: flutter +dev_dependencies: + flutter_test: + sdk: flutter + flutter: plugin: androidPackage: io.flutter.plugins.androidalarmmanager diff --git a/packages/android_alarm_manager/test/android_alarm_manager_test.dart b/packages/android_alarm_manager/test/android_alarm_manager_test.dart new file mode 100644 index 000000000000..1f9d2856838e --- /dev/null +++ b/packages/android_alarm_manager/test/android_alarm_manager_test.dart @@ -0,0 +1,201 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui'; + +import 'package:android_alarm_manager/android_alarm_manager.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + String invalidCallback(String foo) => foo; + void validCallback(int id) => null; + + const MethodChannel testChannel = MethodChannel( + 'plugins.flutter.io/android_alarm_manager', JSONMethodCodec()); + TestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() { + testChannel.setMockMethodCallHandler((MethodCall call) => null); + }); + + test('${AndroidAlarmManager.initialize}', () async { + testChannel.setMockMethodCallHandler((MethodCall call) async { + assert(call.method == 'AlarmService.start'); + return true; + }); + + final bool initialized = await AndroidAlarmManager.initialize(); + + expect(initialized, isTrue); + }); + + group('${AndroidAlarmManager.oneShotAt}', () { + test('validates input', () async { + final DateTime validTime = DateTime.utc(1993); + final int validId = 1; + + // Callback should take a single int param. + await expectLater( + () => AndroidAlarmManager.oneShotAt( + validTime, validId, invalidCallback), + throwsAssertionError); + + // ID should be less than 32 bits. + await expectLater( + () => AndroidAlarmManager.oneShotAt( + validTime, 2147483648, validCallback), + throwsAssertionError); + }); + + test('sends arguments to the platform', () async { + final DateTime alarm = DateTime(1993); + const int rawHandle = 4; + AndroidAlarmManager.setTestOverides( + getCallbackHandle: (Function _) => + CallbackHandle.fromRawHandle(rawHandle)); + + final int id = 1; + final bool alarmClock = true; + final bool allowWhileIdle = true; + final bool exact = true; + final bool wakeup = true; + final bool rescheduleOnReboot = true; + + testChannel.setMockMethodCallHandler((MethodCall call) async { + expect(call.method, 'Alarm.oneShotAt'); + expect(call.arguments[0], id); + expect(call.arguments[1], alarmClock); + expect(call.arguments[2], allowWhileIdle); + expect(call.arguments[3], exact); + expect(call.arguments[4], wakeup); + expect(call.arguments[5], alarm.millisecondsSinceEpoch); + expect(call.arguments[6], rescheduleOnReboot); + expect(call.arguments[7], rawHandle); + return true; + }); + + final bool result = await AndroidAlarmManager.oneShotAt( + alarm, id, validCallback, + alarmClock: alarmClock, + allowWhileIdle: allowWhileIdle, + exact: exact, + wakeup: wakeup, + rescheduleOnReboot: rescheduleOnReboot); + + expect(result, isTrue); + }); + }); + + test('${AndroidAlarmManager.oneShot} calls through to oneShotAt', () async { + final DateTime now = DateTime(1993); + const int rawHandle = 4; + AndroidAlarmManager.setTestOverides( + now: () => now, + getCallbackHandle: (Function _) => + CallbackHandle.fromRawHandle(rawHandle)); + + const Duration alarm = Duration(seconds: 1); + final int id = 1; + final bool alarmClock = true; + final bool allowWhileIdle = true; + final bool exact = true; + final bool wakeup = true; + final bool rescheduleOnReboot = true; + + testChannel.setMockMethodCallHandler((MethodCall call) async { + expect(call.method, 'Alarm.oneShotAt'); + expect(call.arguments[0], id); + expect(call.arguments[1], alarmClock); + expect(call.arguments[2], allowWhileIdle); + expect(call.arguments[3], exact); + expect(call.arguments[4], wakeup); + expect( + call.arguments[5], now.millisecondsSinceEpoch + alarm.inMilliseconds); + expect(call.arguments[6], rescheduleOnReboot); + expect(call.arguments[7], rawHandle); + return true; + }); + + final bool result = await AndroidAlarmManager.oneShot( + alarm, id, validCallback, + alarmClock: alarmClock, + allowWhileIdle: allowWhileIdle, + exact: exact, + wakeup: wakeup, + rescheduleOnReboot: rescheduleOnReboot); + + expect(result, isTrue); + }); + + group('${AndroidAlarmManager.periodic}', () { + test('validates input', () async { + const Duration validDuration = Duration(seconds: 0); + final int validId = 1; + + // Callback should take a single int param. + await expectLater( + () => AndroidAlarmManager.periodic( + validDuration, validId, invalidCallback), + throwsAssertionError); + + // ID should be less than 32 bits. + await expectLater( + () => AndroidAlarmManager.periodic( + validDuration, 2147483648, validCallback), + throwsAssertionError); + }); + + test('sends arguments through to the platform', () async { + final DateTime now = DateTime(1993); + const int rawHandle = 4; + AndroidAlarmManager.setTestOverides( + now: () => now, + getCallbackHandle: (Function _) => + CallbackHandle.fromRawHandle(rawHandle)); + + final int id = 1; + final bool exact = true; + final bool wakeup = true; + final bool rescheduleOnReboot = true; + const Duration period = Duration(seconds: 1); + + testChannel.setMockMethodCallHandler((MethodCall call) async { + expect(call.method, 'Alarm.periodic'); + expect(call.arguments[0], id); + expect(call.arguments[1], exact); + expect(call.arguments[2], wakeup); + expect(call.arguments[3], + (now.millisecondsSinceEpoch + period.inMilliseconds)); + expect(call.arguments[4], period.inMilliseconds); + expect(call.arguments[5], rescheduleOnReboot); + expect(call.arguments[6], rawHandle); + return true; + }); + + final bool result = await AndroidAlarmManager.periodic( + period, + id, + (int id) => null, + exact: exact, + wakeup: wakeup, + rescheduleOnReboot: rescheduleOnReboot, + ); + + expect(result, isTrue); + }); + }); + + test('${AndroidAlarmManager.cancel}', () async { + final int id = 1; + testChannel.setMockMethodCallHandler((MethodCall call) async { + assert(call.method == 'Alarm.cancel' && call.arguments[0] == id); + return true; + }); + + final bool canceled = await AndroidAlarmManager.cancel(id); + + expect(canceled, isTrue); + }); +} diff --git a/packages/android_intent/CHANGELOG.md b/packages/android_intent/CHANGELOG.md index a71b5e943ffa..76db8d19fd8a 100644 --- a/packages/android_intent/CHANGELOG.md +++ b/packages/android_intent/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.3.4+7 + +* Fix pedantic linter errors. + +## 0.3.4+6 + +* Add missing DartDocs for public members. + ## 0.3.4+5 * Remove AndroidX warning. diff --git a/packages/android_intent/example/lib/main.dart b/packages/android_intent/example/lib/main.dart index f56cffd2bd20..45de9632e975 100644 --- a/packages/android_intent/example/lib/main.dart +++ b/packages/android_intent/example/lib/main.dart @@ -11,6 +11,7 @@ void main() { runApp(MyApp()); } +/// A sample app for launching intents. class MyApp extends StatelessWidget { // This widget is the root of your application. @override @@ -29,6 +30,7 @@ class MyApp extends StatelessWidget { } } +/// Holds the different intent widgets. class MyHomePage extends StatelessWidget { void _createAlarm() { final AndroidIntent intent = const AndroidIntent( @@ -80,9 +82,11 @@ class MyHomePage extends StatelessWidget { } } +/// Launches intents to specific Android activities. class ExplicitIntentsWidget extends StatelessWidget { - const ExplicitIntentsWidget(); + const ExplicitIntentsWidget(); // ignore: public_member_api_docs + // ignore: public_member_api_docs static const String routeName = "/explicitIntents"; void _openGoogleMapsStreetView() { diff --git a/packages/android_intent/example/test_driver/android_intent_e2e.dart b/packages/android_intent/example/test_driver/android_intent_e2e.dart index 8df8146f52be..2c45262147b1 100644 --- a/packages/android_intent/example/test_driver/android_intent_e2e.dart +++ b/packages/android_intent/example/test_driver/android_intent_e2e.dart @@ -1,13 +1,12 @@ import 'dart:io'; +import 'package:android_intent/android_intent.dart'; import 'package:android_intent_example/main.dart'; import 'package:e2e/e2e.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../../lib/android_intent.dart'; - /// This is a smoke test that verifies that the example app builds and loads. /// Because this plugin works by launching Android platform UIs it's not /// possible to meaningfully test it through its Dart interface currently. There diff --git a/packages/android_intent/example/test_driver/android_intent_e2e_test.dart b/packages/android_intent/example/test_driver/android_intent_e2e_test.dart index 4f38746ce76c..6147d44df2ec 100644 --- a/packages/android_intent/example/test_driver/android_intent_e2e_test.dart +++ b/packages/android_intent/example/test_driver/android_intent_e2e_test.dart @@ -7,6 +7,6 @@ Future main() async { final FlutterDriver driver = await FlutterDriver.connect(); final String result = await driver.requestData(null, timeout: const Duration(minutes: 1)); - driver.close(); + await driver.close(); exit(result == 'pass' ? 0 : 1); } diff --git a/packages/android_intent/lib/android_intent.dart b/packages/android_intent/lib/android_intent.dart index 9c036cf98e15..1ba8d3d15ea5 100644 --- a/packages/android_intent/lib/android_intent.dart +++ b/packages/android_intent/lib/android_intent.dart @@ -8,9 +8,13 @@ import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; import 'package:platform/platform.dart'; -const String kChannelName = 'plugins.flutter.io/android_intent'; +const String _kChannelName = 'plugins.flutter.io/android_intent'; /// Flutter plugin for launching arbitrary Android Intents. +/// +/// See [the official Android +/// documentation](https://developer.android.com/reference/android/content/Intent.html) +/// for more information on how to use Intents. class AndroidIntent { /// Builds an Android intent with the following parameters /// [action] refers to the action parameter of the intent. @@ -33,9 +37,11 @@ class AndroidIntent { this.componentName, Platform platform, }) : assert(action != null), - _channel = const MethodChannel(kChannelName), + _channel = const MethodChannel(_kChannelName), _platform = platform ?? const LocalPlatform(); + /// This constructor is only exposed for unit testing. Do not rely on this in + /// app code, it may break without warning. @visibleForTesting AndroidIntent.private({ @required this.action, @@ -50,12 +56,43 @@ class AndroidIntent { }) : _channel = channel, _platform = platform; + /// This is the general verb that the intent should attempt to do. This + /// includes constants like `ACTION_VIEW`. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. final String action; + + /// Constants that can be set on an intent to tweak how it is finally handled. + /// Some of the constants are mirrored to Dart via [Flag]. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#setFlags(int). final List flags; + + /// An optional additional constant qualifying the given [action]. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. final String category; + + /// The Uri that the [action] is pointed towards. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. final String data; + + /// The equivalent of `extras`, a generic `Bundle` of data that the Intent can + /// carry. This is a slot for extraneous data that the listener may use. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. final Map arguments; + + /// Sets the [data] to only resolve within this given package. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#setPackage(java.lang.String). final String package; + + /// Set the exact `ComponentName` that should handle the intent. If this is + /// set [package] should also be non-null. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#setComponent(android.content.ComponentName). final String componentName; final MethodChannel _channel; final Platform _platform; @@ -65,6 +102,8 @@ class AndroidIntent { return x != 0 && ((x & (x - 1)) == 0); } + /// This method is just visible for unit testing and should not be relied on. + /// Its method signature may change at any time. @visibleForTesting int convertFlags(List flags) { int finalValue = 0; diff --git a/packages/android_intent/lib/flag.dart b/packages/android_intent/lib/flag.dart index b4e6ed100146..e05aa6d12666 100644 --- a/packages/android_intent/lib/flag.dart +++ b/packages/android_intent/lib/flag.dart @@ -1,37 +1,193 @@ -// flag values from https://developer.android.com/reference/android/content/Intent.html +/// Special flags that can be set on an intent to control how it is handled. +/// +/// See +/// https://developer.android.com/reference/android/content/Intent.html#setFlags(int) +/// for the official documentation on Intent flags. The constants here mirror +/// the existing [android.content.Intent] ones. class Flag { + /// Specifies how an activity should be launched. Generally set by the system + /// in conjunction with SINGLE_TASK. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_BROUGHT_TO_FRONT. static const int FLAG_ACTIVITY_BROUGHT_TO_FRONT = 4194304; + + /// Causes any existing tasks associated with the activity to be cleared. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_CLEAR_TASK static const int FLAG_ACTIVITY_CLEAR_TASK = 32768; + + /// Closes any activities on top of this activity and brings it to the front, + /// if it's currently running. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_CLEAR_TOP static const int FLAG_ACTIVITY_CLEAR_TOP = 67108864; + + /// @deprecated Use [FLAG_ACTIVITY_NEW_DOCUMENT] instead when on API 21 or above. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET + @deprecated static const int FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET = 524288; + + /// Keeps the activity from being listed with other recently launched + /// activities. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS static const int FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS = 8388608; + + /// Forwards the result from this activity to the existing one. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_FORWARD_RESULT static const int FLAG_ACTIVITY_FORWARD_RESULT = 33554432; + + /// Generally set by the system if the activity is being launched from + /// history. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY static const int FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY = 1048576; + + /// Used in split-screen mode to set the launched activity adjacent to the + /// launcher. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_LAUNCH_ADJACENT static const int FLAG_ACTIVITY_LAUNCH_ADJACENT = 4096; + + /// Used in split-screen mode to set the launched activity adjacent to the + /// launcher. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_MATCH_EXTERNAL static const int FLAG_ACTIVITY_MATCH_EXTERNAL = 2048; + + /// Creates and launches the activity into a new task. Should always be + /// combined with [FLAG_ACTIVITY_NEW_DOCUMENT] or [FLAG_ACTIVITY_NEW_TASK]. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_MULTIPLE_TASK. static const int FLAG_ACTIVITY_MULTIPLE_TASK = 134217728; + + /// Opens a document into a new task rooted in this activity. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NEW_DOCUMENT. static const int FLAG_ACTIVITY_NEW_DOCUMENT = 524288; + + /// The launched activity starts a new task on the activity stack. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NEW_TASK. static const int FLAG_ACTIVITY_NEW_TASK = 268435456; + + /// Prevents the system from playing an activity transition animation when + /// launching this. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NO_ANIMATION. static const int FLAG_ACTIVITY_NO_ANIMATION = 65536; + + /// Does not keep the launched activity in history. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NO_HISTORY. static const int FLAG_ACTIVITY_NO_HISTORY = 1073741824; + + /// Prevents a typical callback from occuring when the activity is paused. + /// + /// https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NO_USER_ACTION static const int FLAG_ACTIVITY_NO_USER_ACTION = 262144; + + /// Uses the previous activity as top when applicable. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_PREVIOUS_IS_TOP. static const int FLAG_ACTIVITY_PREVIOUS_IS_TOP = 16777216; + + /// Brings any already instances of this activity to the front. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_REORDER_TO_FRONT. static const int FLAG_ACTIVITY_REORDER_TO_FRONT = 131072; + + /// Launches the activity in a way that resets the task in some cases. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_RESET_TASK_IF_NEEDED. static const int FLAG_ACTIVITY_RESET_TASK_IF_NEEDED = 2097152; + + /// Keeps an entry in recent tasks. Used with [FLAG_ACTIVITY_NEW_DOCUMENT]. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_RETAIN_IN_RECENTS. static const int FLAG_ACTIVITY_RETAIN_IN_RECENTS = 8192; + + /// Will not re-launch the activity if it is already at the top of the history + /// stack. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_SINGLE_TOP. static const int FLAG_ACTIVITY_SINGLE_TOP = 536870912; + + /// Places the activity on top of the home task. Must be used with + /// [FLAG_ACTIVITY_NEW_TASK]. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_TASK_ON_HOME. static const int FLAG_ACTIVITY_TASK_ON_HOME = 16384; + + /// Prints debug logs while the intent is resolving. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_DEBUG_LOG_RESOLUTION. static const int FLAG_DEBUG_LOG_RESOLUTION = 8; + + /// Does not match to any stopped components. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_EXCLUDE_STOPPED_PACKAGES. static const int FLAG_EXCLUDE_STOPPED_PACKAGES = 16; + + /// Can be set by the caller to flag the intent as not being launched directly + /// by the user. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_FROM_BACKGROUND. static const int FLAG_FROM_BACKGROUND = 4; + + /// Will persist the URI permision across device reboots. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_GRANT_PERSISTABLE_URI_PERMISSION. static const int FLAG_GRANT_PERSISTABLE_URI_PERMISSION = 64; + + /// Applies the URI permission grant based on prefix matching. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_GRANT_PREFIX_URI_PERMISSION. static const int FLAG_GRANT_PREFIX_URI_PERMISSION = 128; + + /// Grants the intent listener permission to read extra data from the Intent's + /// URI. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_GRANT_READ_URI_PERMISSION. static const int FLAG_GRANT_READ_URI_PERMISSION = 1; + + /// Grants the intent listener permission to write extra data from the + /// Intent's URI. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_GRANT_WRITE_URI_PERMISSION. static const int FLAG_GRANT_WRITE_URI_PERMISSION = 2; + + /// Always matches stopped components. This is the default behavior. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_INCLUDE_STOPPED_PACKAGES. static const int FLAG_INCLUDE_STOPPED_PACKAGES = 32; + + /// Allows the listener to run at a high priority. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_RECEIVER_FOREGROUND. static const int FLAG_RECEIVER_FOREGROUND = 268435456; + + /// Doesn't allow listeners to cancel the broadcast. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_RECEIVER_NO_ABORT. static const int FLAG_RECEIVER_NO_ABORT = 134217728; + + /// Only allows registered receivers to listen for the intent. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_RECEIVER_REGISTERED_ONLY. static const int FLAG_RECEIVER_REGISTERED_ONLY = 1073741824; + + /// Will drop any pending broadcasts of this intent in favor of the newest + /// one. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_RECEIVER_REPLACE_PENDING. static const int FLAG_RECEIVER_REPLACE_PENDING = 536870912; + + /// Instant Apps will be able to listen for the intent (not the default + /// behavior). + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS. static const int FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS = 2097152; } diff --git a/packages/android_intent/pubspec.yaml b/packages/android_intent/pubspec.yaml index b0248ad2a5cd..85b83b5a8f0a 100644 --- a/packages/android_intent/pubspec.yaml +++ b/packages/android_intent/pubspec.yaml @@ -2,7 +2,7 @@ name: android_intent description: Flutter plugin for launching Android Intents. Not supported on iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/android_intent -version: 0.3.4+5 +version: 0.3.4+7 flutter: plugin: diff --git a/packages/android_intent/test/android_intent_test.dart b/packages/android_intent/test/android_intent_test.dart index b13438bf7469..5562159d7bcc 100644 --- a/packages/android_intent/test/android_intent_test.dart +++ b/packages/android_intent/test/android_intent_test.dart @@ -23,7 +23,7 @@ void main() { flags: [Flag.FLAG_ACTIVITY_NEW_TASK], channel: mockChannel, platform: FakePlatform(operatingSystem: 'android')); - androidIntent.launch(); + await androidIntent.launch(); verify(mockChannel.invokeMethod('launch', { 'action': 'action_view', 'data': Uri.encodeFull('https://flutter.io'), @@ -35,7 +35,7 @@ void main() { action: null, channel: mockChannel, platform: FakePlatform(operatingSystem: 'android')); - androidIntent.launch(); + await androidIntent.launch(); verify(mockChannel.invokeMethod('launch', { 'action': null, })); @@ -46,7 +46,7 @@ void main() { action: null, channel: mockChannel, platform: FakePlatform(operatingSystem: 'ios')); - androidIntent.launch(); + await androidIntent.launch(); verifyZeroInteractions(mockChannel); }); }); diff --git a/packages/battery/CHANGELOG.md b/packages/battery/CHANGELOG.md index c6381ec0282e..12a830069fcb 100644 --- a/packages/battery/CHANGELOG.md +++ b/packages/battery/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.1+4 + +* Update and migrate iOS example project. + ## 0.3.1+3 * Remove AndroidX warning. diff --git a/packages/battery/analysis_options.yaml b/packages/battery/analysis_options.yaml new file mode 100644 index 000000000000..d4ccef63f1d1 --- /dev/null +++ b/packages/battery/analysis_options.yaml @@ -0,0 +1,11 @@ +# This is a temporary file to allow us to land a new set of linter rules in a +# series of manageable patches instead of one gigantic PR. It disables some of +# the new lints that are already failing on this plugin, for this plugin. It +# should be deleted and the failing lints addressed as soon as possible. + +include: ../../analysis_options.yaml + +analyzer: + errors: + public_member_api_docs: ignore + unawaited_futures: ignore diff --git a/packages/battery/example/ios/Runner.xcodeproj/project.pbxproj b/packages/battery/example/ios/Runner.xcodeproj/project.pbxproj index 235e8749805d..aa42a8509346 100644 --- a/packages/battery/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/battery/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,19 +9,16 @@ /* 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 */; }; - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 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 */; }; - EA987CF1DD05781B010B5D39 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B1E864018968B8D5FA44E86 /* Pods_Runner.framework */; }; + CC33A11108F15DB5F0C6C7AD /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E2CD29898079A0E658445A5 /* libPods-Runner.a */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -42,10 +39,10 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1E2CD29898079A0E658445A5 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 4B1E864018968B8D5FA44E86 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5F92487ECF695372E82D90C5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -58,6 +55,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BF850F5DC44F7AE2B245B994 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -67,7 +65,7 @@ files = ( 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - EA987CF1DD05781B010B5D39 /* Pods_Runner.framework in Frameworks */, + CC33A11108F15DB5F0C6C7AD /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -77,7 +75,7 @@ 1C99224A167BC35DA0CD0913 /* Frameworks */ = { isa = PBXGroup; children = ( - 4B1E864018968B8D5FA44E86 /* Pods_Runner.framework */, + 1E2CD29898079A0E658445A5 /* libPods-Runner.a */, ); name = Frameworks; sourceTree = ""; @@ -85,6 +83,8 @@ 571753FC2D526E56A295E627 /* Pods */ = { isa = PBXGroup; children = ( + 5F92487ECF695372E82D90C5 /* Pods-Runner.debug.xcconfig */, + BF850F5DC44F7AE2B245B994 /* Pods-Runner.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -94,7 +94,6 @@ children = ( 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -161,7 +160,6 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 4096151B6BA12D6D4D7DD96A /* [CP] Embed Pods Frameworks */, - 7C9CC6394B25E69B476E302B /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -178,18 +176,17 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0830; + LastUpgradeCheck = 1100; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = 3GRKCVVJ22; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -211,10 +208,7 @@ buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); @@ -249,22 +243,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 7C9CC6394B25E69B476E302B /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { @@ -287,13 +266,16 @@ 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"; + 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 */ @@ -336,19 +318,28 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -384,19 +375,28 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -425,9 +425,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { - ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = 3GRKCVVJ22; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -448,9 +446,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = 3GRKCVVJ22; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/packages/battery/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/battery/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1c9580788197..3bb3697ef41c 100644 --- a/packages/battery/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/battery/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + - - homepage: https://github.com/flutter/plugins/tree/master/packages/battery -version: 0.3.1+3 +version: 0.3.1+4 flutter: plugin: diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md index 8a3f248433eb..84c05e9c75c5 100644 --- a/packages/camera/CHANGELOG.md +++ b/packages/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.7 + +* Fix unawaited futures. + ## 0.5.6+4 * Android: Use CameraDevice.TEMPLATE_RECORD to improve image streaming. diff --git a/packages/camera/analysis_options.yaml b/packages/camera/analysis_options.yaml new file mode 100644 index 000000000000..8e4af76f0a30 --- /dev/null +++ b/packages/camera/analysis_options.yaml @@ -0,0 +1,10 @@ +# This is a temporary file to allow us to land a new set of linter rules in a +# series of manageable patches instead of one gigantic PR. It disables some of +# the new lints that are already failing on this plugin, for this plugin. It +# should be deleted and the failing lints addressed as soon as possible. + +include: ../../analysis_options.yaml + +analyzer: + errors: + public_member_api_docs: ignore diff --git a/packages/camera/example/lib/main.dart b/packages/camera/example/lib/main.dart index 1c4b11672530..9383a2098ad0 100644 --- a/packages/camera/example/lib/main.dart +++ b/packages/camera/example/lib/main.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: public_member_api_docs + import 'dart:async'; import 'dart:io'; diff --git a/packages/camera/example/test_driver/camera_e2e.dart b/packages/camera/example/test_driver/camera_e2e.dart index 151339942f15..a1cc8ad9ca02 100644 --- a/packages/camera/example/test_driver/camera_e2e.dart +++ b/packages/camera/example/test_driver/camera_e2e.dart @@ -231,7 +231,7 @@ void main() { sleep(const Duration(milliseconds: 500)); await controller.stopImageStream(); - controller.dispose(); + await controller.dispose(); }, skip: !Platform.isAndroid, ); diff --git a/packages/camera/example/test_driver/camera_e2e_test.dart b/packages/camera/example/test_driver/camera_e2e_test.dart index e3e089a81fc9..4963854dea72 100644 --- a/packages/camera/example/test_driver/camera_e2e_test.dart +++ b/packages/camera/example/test_driver/camera_e2e_test.dart @@ -35,7 +35,7 @@ Future main() async { final FlutterDriver driver = await FlutterDriver.connect(); final String result = await driver.requestData(null, timeout: const Duration(minutes: 1)); - driver.close(); + await driver.close(); print('Test finished. Revoking camera permissions...'); Process.runSync('adb', [ 'shell', diff --git a/packages/camera/lib/camera.dart b/packages/camera/lib/camera.dart index ee1892c4cbc0..ce9fd9430dde 100644 --- a/packages/camera/lib/camera.dart +++ b/packages/camera/lib/camera.dart @@ -38,6 +38,7 @@ enum ResolutionPreset { max, } +// ignore: inference_failure_on_function_return_type typedef onLatestImageAvailable = Function(CameraImage image); /// Returns the resolution preset as a String. @@ -444,7 +445,7 @@ class CameraController extends ValueNotifier { throw CameraException(e.code, e.message); } - _imageStreamSubscription.cancel(); + await _imageStreamSubscription.cancel(); _imageStreamSubscription = null; } diff --git a/packages/camera/pubspec.yaml b/packages/camera/pubspec.yaml index aae2d6e9112b..431e3344f6da 100644 --- a/packages/camera/pubspec.yaml +++ b/packages/camera/pubspec.yaml @@ -2,7 +2,7 @@ name: camera description: A Flutter plugin for getting information about and controlling the camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video, and streaming image buffers to dart. -version: 0.5.6+3 +version: 0.5.7 authors: - Flutter Team diff --git a/packages/connectivity/CHANGELOG.md b/packages/connectivity/CHANGELOG.md index 5a7b5f7f4e05..0e1ea814907a 100644 --- a/packages/connectivity/CHANGELOG.md +++ b/packages/connectivity/CHANGELOG.md @@ -1,3 +1,15 @@ +## 0.4.5+6 + +* Fix singleton Reachability problem on iOS. + +## 0.4.5+5 + +* Add an analyzer check for the public documentation. + +## 0.4.5+4 + +* Stability and Maintainability: update documentations. + ## 0.4.5+3 * Remove AndroidX warnings. diff --git a/packages/connectivity/analysis_options.yaml b/packages/connectivity/analysis_options.yaml new file mode 100644 index 000000000000..22c4e1041011 --- /dev/null +++ b/packages/connectivity/analysis_options.yaml @@ -0,0 +1,10 @@ +# This is a temporary file to allow us to land a new set of linter rules in a +# series of manageable patches instead of one gigantic PR. It disables some of +# the new lints that are already failing on this plugin, for this plugin. It +# should be deleted and the failing lints addressed as soon as possible. + +include: ../../analysis_options.yaml + +analyzer: + errors: + unawaited_futures: ignore diff --git a/packages/connectivity/example/README.md b/packages/connectivity/example/README.md deleted file mode 100644 index a7bac8c32e46..000000000000 --- a/packages/connectivity/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# connectivity_example - -Demonstrates how to use the connectivity plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](http://flutter.io/). \ No newline at end of file diff --git a/packages/connectivity/example/lib/main.dart b/packages/connectivity/example/lib/main.dart index c01a110efb60..c67e90881e02 100644 --- a/packages/connectivity/example/lib/main.dart +++ b/packages/connectivity/example/lib/main.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: public_member_api_docs + import 'dart:async'; import 'dart:io'; diff --git a/packages/connectivity/ios/Classes/ConnectivityPlugin.m b/packages/connectivity/ios/Classes/ConnectivityPlugin.m index c69871175b01..100e70b44ec6 100644 --- a/packages/connectivity/ios/Classes/ConnectivityPlugin.m +++ b/packages/connectivity/ios/Classes/ConnectivityPlugin.m @@ -22,6 +22,7 @@ @interface FLTConnectivityPlugin () *)registrar { @@ -174,12 +175,16 @@ - (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink selector:@selector(onReachabilityDidChange:) name:kReachabilityChangedNotification object:nil]; - [[Reachability reachabilityForInternetConnection] startNotifier]; + _reachabilityForInternetConnection = [Reachability reachabilityForInternetConnection]; + [_reachabilityForInternetConnection startNotifier]; return nil; } - (FlutterError*)onCancelWithArguments:(id)arguments { - [[Reachability reachabilityForInternetConnection] stopNotifier]; + if (_reachabilityForInternetConnection) { + [_reachabilityForInternetConnection stopNotifier]; + _reachabilityForInternetConnection = nil; + } [[NSNotificationCenter defaultCenter] removeObserver:self]; _eventSink = nil; return nil; diff --git a/packages/connectivity/lib/connectivity.dart b/packages/connectivity/lib/connectivity.dart index 03659f5455a9..ad9fae317012 100644 --- a/packages/connectivity/lib/connectivity.dart +++ b/packages/connectivity/lib/connectivity.dart @@ -8,13 +8,19 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; -/// Connection Status Check Result -/// -/// WiFi: Device connected via Wi-Fi -/// Mobile: Device connected to cellular network -/// None: Device not connected to any network -enum ConnectivityResult { wifi, mobile, none } +/// Connection status check result. +enum ConnectivityResult { + /// WiFi: Device connected via Wi-Fi + wifi, + /// Mobile: Device connected to cellular network + mobile, + + /// None: Device not connected to any network + none +} + +/// Discover network connectivity configurations: Distinguish between WI-FI and cellular, check WI-FI status and more. class Connectivity { /// Constructs a singleton instance of [Connectivity]. /// @@ -35,11 +41,13 @@ class Connectivity { Stream _onConnectivityChanged; + /// Exposed for testing purposes and should not be used by users of the plugin. @visibleForTesting static const MethodChannel methodChannel = MethodChannel( 'plugins.flutter.io/connectivity', ); + /// Exposed for testing purposes and should not be used by users of the plugin. @visibleForTesting static const EventChannel eventChannel = EventChannel( 'plugins.flutter.io/connectivity_status', diff --git a/packages/connectivity/pubspec.yaml b/packages/connectivity/pubspec.yaml index 4466c7ef6054..8dce3c8867b6 100644 --- a/packages/connectivity/pubspec.yaml +++ b/packages/connectivity/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for discovering the state of the network (WiFi & mobile/cellular) connectivity on Android and iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/connectivity -version: 0.4.5+3 +version: 0.4.5+6 flutter: plugin: diff --git a/packages/device_info/analysis_options.yaml b/packages/device_info/analysis_options.yaml new file mode 100644 index 000000000000..d4ccef63f1d1 --- /dev/null +++ b/packages/device_info/analysis_options.yaml @@ -0,0 +1,11 @@ +# This is a temporary file to allow us to land a new set of linter rules in a +# series of manageable patches instead of one gigantic PR. It disables some of +# the new lints that are already failing on this plugin, for this plugin. It +# should be deleted and the failing lints addressed as soon as possible. + +include: ../../analysis_options.yaml + +analyzer: + errors: + public_member_api_docs: ignore + unawaited_futures: ignore diff --git a/packages/e2e/analysis_options.yaml b/packages/e2e/analysis_options.yaml new file mode 100644 index 000000000000..d4ccef63f1d1 --- /dev/null +++ b/packages/e2e/analysis_options.yaml @@ -0,0 +1,11 @@ +# This is a temporary file to allow us to land a new set of linter rules in a +# series of manageable patches instead of one gigantic PR. It disables some of +# the new lints that are already failing on this plugin, for this plugin. It +# should be deleted and the failing lints addressed as soon as possible. + +include: ../../analysis_options.yaml + +analyzer: + errors: + public_member_api_docs: ignore + unawaited_futures: ignore diff --git a/packages/e2e/example/pubspec.yaml b/packages/e2e/example/pubspec.yaml index 456a62f2b36a..433fe782ad5e 100644 --- a/packages/e2e/example/pubspec.yaml +++ b/packages/e2e/example/pubspec.yaml @@ -1,5 +1,6 @@ name: e2e_example description: Demonstrates how to use the e2e plugin. +version: 0.0.1 publish_to: 'none' environment: diff --git a/packages/flutter_plugin_android_lifecycle/example/lib/main.dart b/packages/flutter_plugin_android_lifecycle/example/lib/main.dart index 12339516b156..01da8d66fc0d 100644 --- a/packages/flutter_plugin_android_lifecycle/example/lib/main.dart +++ b/packages/flutter_plugin_android_lifecycle/example/lib/main.dart @@ -1,3 +1,5 @@ +// ignore_for_file: public_member_api_docs + import 'package:flutter/material.dart'; void main() => runApp(MyApp()); diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index 2c83c6449da9..0bf3943256ed 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.5.21+12 + +* Update driver tests in the example app to e2e tests. + +## 0.5.21+11 + +* Define clang module for iOS, fix analyzer warnings. + ## 0.5.21+10 * Cast error.code to unsigned long to avoid using NSInteger as %ld format warnings. diff --git a/packages/google_maps_flutter/analysis_options.yaml b/packages/google_maps_flutter/analysis_options.yaml new file mode 100644 index 000000000000..d4ccef63f1d1 --- /dev/null +++ b/packages/google_maps_flutter/analysis_options.yaml @@ -0,0 +1,11 @@ +# This is a temporary file to allow us to land a new set of linter rules in a +# series of manageable patches instead of one gigantic PR. It disables some of +# the new lints that are already failing on this plugin, for this plugin. It +# should be deleted and the failing lints addressed as soon as possible. + +include: ../../analysis_options.yaml + +analyzer: + errors: + public_member_api_docs: ignore + unawaited_futures: ignore diff --git a/packages/google_maps_flutter/android/build.gradle b/packages/google_maps_flutter/android/build.gradle index f13de0b79a18..9baaea837d11 100644 --- a/packages/google_maps_flutter/android/build.gradle +++ b/packages/google_maps_flutter/android/build.gradle @@ -36,3 +36,29 @@ android { implementation 'com.google.android.gms:play-services-maps:17.0.0' } } + +// TODO(cyanglaz): Remove this hack once androidx.lifecycle is included on stable. https://github.com/flutter/flutter/issues/42348 +afterEvaluate { + def containsEmbeddingDependencies = false + for (def configuration : configurations.all) { + for (def dependency : configuration.dependencies) { + if (dependency.group == 'io.flutter' && + dependency.name.startsWith('flutter_embedding') && + dependency.isTransitive()) + { + containsEmbeddingDependencies = true + break + } + } + } + if (!containsEmbeddingDependencies) { + android { + dependencies { + def lifecycle_version = "1.1.1" + compileOnly "android.arch.lifecycle:runtime:$lifecycle_version" + compileOnly "android.arch.lifecycle:common:$lifecycle_version" + compileOnly "android.arch.lifecycle:common-java8:$lifecycle_version" + } + } + } +} \ No newline at end of file diff --git a/packages/google_maps_flutter/example/ios/Runner/AppDelegate.m b/packages/google_maps_flutter/example/ios/Runner/AppDelegate.m index 893deae584b0..6896c5c190b1 100644 --- a/packages/google_maps_flutter/example/ios/Runner/AppDelegate.m +++ b/packages/google_maps_flutter/example/ios/Runner/AppDelegate.m @@ -1,6 +1,7 @@ -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" -#import "GoogleMaps/GoogleMaps.h" +#import "AppDelegate.h" +#import "GeneratedPluginRegistrant.h" + +@import GoogleMaps; @implementation AppDelegate diff --git a/packages/google_maps_flutter/example/pubspec.yaml b/packages/google_maps_flutter/example/pubspec.yaml index 799d80e76328..7e533a8de3b3 100644 --- a/packages/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/example/pubspec.yaml @@ -15,6 +15,7 @@ dev_dependencies: flutter_driver: sdk: flutter test: ^1.6.0 + e2e: ^0.2.1 # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec diff --git a/packages/google_maps_flutter/example/test_driver/google_maps.dart b/packages/google_maps_flutter/example/test_driver/google_maps_e2e.dart similarity index 86% rename from packages/google_maps_flutter/example/test_driver/google_maps.dart rename to packages/google_maps_flutter/example/test_driver/google_maps_e2e.dart index 90d065fbeb5b..53d0c90f99de 100644 --- a/packages/google_maps_flutter/example/test_driver/google_maps.dart +++ b/packages/google_maps_flutter/example/test_driver/google_maps_e2e.dart @@ -7,29 +7,25 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:e2e/e2e.dart'; import 'google_map_inspector.dart'; -import 'test_widgets.dart'; const LatLng _kInitialMapCenter = LatLng(0, 0); +const double _kInitialZoomLevel = 5; const CameraPosition _kInitialCameraPosition = - CameraPosition(target: _kInitialMapCenter); + CameraPosition(target: _kInitialMapCenter, zoom: _kInitialZoomLevel); void main() { - final Completer allTestsCompleter = Completer(); - enableFlutterDriverExtension(handler: (_) => allTestsCompleter.future); + E2EWidgetsFlutterBinding.ensureInitialized(); - tearDownAll(() => allTestsCompleter.complete(null)); - - test('testCompassToggle', () async { + testWidgets('testCompassToggle', (WidgetTester tester) async { final Key key = GlobalKey(); final Completer inspectorCompleter = Completer(); - - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -48,7 +44,7 @@ void main() { bool compassEnabled = await inspector.isCompassEnabled(); expect(compassEnabled, false); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -64,12 +60,12 @@ void main() { expect(compassEnabled, true); }); - test('testMapToolbarToggle', () async { + testWidgets('testMapToolbarToggle', (WidgetTester tester) async { final Key key = GlobalKey(); final Completer inspectorCompleter = Completer(); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -88,7 +84,7 @@ void main() { bool mapToolbarEnabled = await inspector.isMapToolbarEnabled(); expect(mapToolbarEnabled, false); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -104,7 +100,7 @@ void main() { expect(mapToolbarEnabled, Platform.isAndroid); }); - test('updateMinMaxZoomLevels', () async { + testWidgets('updateMinMaxZoomLevels', (WidgetTester tester) async { final Key key = GlobalKey(); final Completer inspectorCompleter = Completer(); @@ -112,7 +108,7 @@ void main() { const MinMaxZoomPreference initialZoomLevel = MinMaxZoomPreference(2, 4); const MinMaxZoomPreference finalZoomLevel = MinMaxZoomPreference(3, 8); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -131,7 +127,7 @@ void main() { MinMaxZoomPreference zoomLevel = await inspector.getMinMaxZoomLevels(); expect(zoomLevel, equals(initialZoomLevel)); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -147,12 +143,12 @@ void main() { expect(zoomLevel, equals(finalZoomLevel)); }); - test('testZoomGesturesEnabled', () async { + testWidgets('testZoomGesturesEnabled', (WidgetTester tester) async { final Key key = GlobalKey(); final Completer inspectorCompleter = Completer(); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -171,7 +167,7 @@ void main() { bool zoomGesturesEnabled = await inspector.isZoomGesturesEnabled(); expect(zoomGesturesEnabled, false); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -187,12 +183,12 @@ void main() { expect(zoomGesturesEnabled, true); }); - test('testRotateGesturesEnabled', () async { + testWidgets('testRotateGesturesEnabled', (WidgetTester tester) async { final Key key = GlobalKey(); final Completer inspectorCompleter = Completer(); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -211,7 +207,7 @@ void main() { bool rotateGesturesEnabled = await inspector.isRotateGesturesEnabled(); expect(rotateGesturesEnabled, false); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -227,12 +223,12 @@ void main() { expect(rotateGesturesEnabled, true); }); - test('testTiltGesturesEnabled', () async { + testWidgets('testTiltGesturesEnabled', (WidgetTester tester) async { final Key key = GlobalKey(); final Completer inspectorCompleter = Completer(); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -251,7 +247,7 @@ void main() { bool tiltGesturesEnabled = await inspector.isTiltGesturesEnabled(); expect(tiltGesturesEnabled, false); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -267,12 +263,12 @@ void main() { expect(tiltGesturesEnabled, true); }); - test('testScrollGesturesEnabled', () async { + testWidgets('testScrollGesturesEnabled', (WidgetTester tester) async { final Key key = GlobalKey(); final Completer inspectorCompleter = Completer(); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -291,7 +287,7 @@ void main() { bool scrollGesturesEnabled = await inspector.isScrollGesturesEnabled(); expect(scrollGesturesEnabled, false); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -307,7 +303,7 @@ void main() { expect(scrollGesturesEnabled, true); }); - test('testGetVisibleRegion', () async { + testWidgets('testGetVisibleRegion', (WidgetTester tester) async { final Key key = GlobalKey(); final LatLngBounds zeroLatLngBounds = LatLngBounds( southwest: const LatLng(0, 0), northeast: const LatLng(0, 0)); @@ -315,7 +311,7 @@ void main() { final Completer mapControllerCompleter = Completer(); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -325,16 +321,15 @@ void main() { }, ), )); - final GoogleMapController mapController = - await mapControllerCompleter.future; - // We suspected a bug in the iOS Google Maps SDK caused the camera is not properly positioned at // initialization. https://github.com/flutter/flutter/issues/24806 // This temporary workaround fix is provided while the actual fix in the Google Maps SDK is // still being investigated. // TODO(cyanglaz): Remove this temporary fix once the Maps SDK issue is resolved. // https://github.com/flutter/flutter/issues/27550 - await Future.delayed(const Duration(seconds: 3)); + await tester.pumpAndSettle(const Duration(seconds: 3)); + final GoogleMapController mapController = + await mapControllerCompleter.future; final LatLngBounds firstVisibleRegion = await mapController.getVisibleRegion(); @@ -345,8 +340,12 @@ void main() { expect(firstVisibleRegion, isNot(zeroLatLngBounds)); expect(firstVisibleRegion.contains(_kInitialMapCenter), isTrue); - const LatLng southWest = LatLng(60, 75); - const LatLng northEast = LatLng(65, 80); + // Making a new `LatLngBounds` about (10, 10) distance south west to the `firstVisibleRegion`. + // The size of the `LatLngBounds` is 10 by 10. + final LatLng southWest = LatLng(firstVisibleRegion.southwest.latitude - 20, + firstVisibleRegion.southwest.longitude - 20); + final LatLng northEast = LatLng(firstVisibleRegion.southwest.latitude - 10, + firstVisibleRegion.southwest.longitude - 10); final LatLng newCenter = LatLng( (northEast.latitude + southWest.latitude) / 2, (northEast.longitude + southWest.longitude) / 2, @@ -363,6 +362,7 @@ void main() { final double padding = 0; await mapController .moveCamera(CameraUpdate.newLatLngBounds(latLngBounds, padding)); + await tester.pumpAndSettle(const Duration(seconds: 3)); final LatLngBounds secondVisibleRegion = await mapController.getVisibleRegion(); @@ -376,12 +376,12 @@ void main() { expect(secondVisibleRegion.contains(newCenter), isTrue); }); - test('testTraffic', () async { + testWidgets('testTraffic', (WidgetTester tester) async { final Key key = GlobalKey(); final Completer inspectorCompleter = Completer(); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -401,12 +401,12 @@ void main() { expect(isTrafficEnabled, true); }); - test('testMyLocationButtonToggle', () async { + testWidgets('testMyLocationButtonToggle', (WidgetTester tester) async { final Key key = GlobalKey(); final Completer inspectorCompleter = Completer(); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -426,7 +426,7 @@ void main() { bool myLocationButtonEnabled = await inspector.isMyLocationButtonEnabled(); expect(myLocationButtonEnabled, true); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -443,12 +443,13 @@ void main() { expect(myLocationButtonEnabled, false); }); - test('testMyLocationButton initial value false', () async { + testWidgets('testMyLocationButton initial value false', + (WidgetTester tester) async { final Key key = GlobalKey(); final Completer inspectorCompleter = Completer(); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -470,12 +471,13 @@ void main() { expect(myLocationButtonEnabled, false); }); - test('testMyLocationButton initial value true', () async { + testWidgets('testMyLocationButton initial value true', + (WidgetTester tester) async { final Key key = GlobalKey(); final Completer inspectorCompleter = Completer(); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -497,12 +499,12 @@ void main() { expect(myLocationButtonEnabled, true); }); - test('testSetMapStyle valid Json String', () async { + testWidgets('testSetMapStyle valid Json String', (WidgetTester tester) async { final Key key = GlobalKey(); final Completer controllerCompleter = Completer(); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -519,12 +521,13 @@ void main() { await controller.setMapStyle(mapStyle); }); - test('testSetMapStyle invalid Json String', () async { + testWidgets('testSetMapStyle invalid Json String', + (WidgetTester tester) async { final Key key = GlobalKey(); final Completer controllerCompleter = Completer(); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -546,12 +549,12 @@ void main() { } }); - test('testSetMapStyle null string', () async { + testWidgets('testSetMapStyle null string', (WidgetTester tester) async { final Key key = GlobalKey(); final Completer controllerCompleter = Completer(); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -566,12 +569,12 @@ void main() { await controller.setMapStyle(null); }); - test('testGetLatLng', () async { + testWidgets('testGetLatLng', (WidgetTester tester) async { final Key key = GlobalKey(); final Completer controllerCompleter = Completer(); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -590,7 +593,7 @@ void main() { // still being investigated. // TODO(cyanglaz): Remove this temporary fix once the Maps SDK issue is resolved. // https://github.com/flutter/flutter/issues/27550 - await Future.delayed(const Duration(seconds: 3)); + await tester.pumpAndSettle(const Duration(seconds: 3)); final LatLngBounds visibleRegion = await controller.getVisibleRegion(); final LatLng topLeft = @@ -603,12 +606,12 @@ void main() { expect(topLeft, northWest); }); - test('testScreenCoordinate', () async { + testWidgets('testScreenCoordinate', (WidgetTester tester) async { final Key key = GlobalKey(); final Completer controllerCompleter = Completer(); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, @@ -618,7 +621,6 @@ void main() { }, ), )); - final GoogleMapController controller = await controllerCompleter.future; // We suspected a bug in the iOS Google Maps SDK caused the camera is not properly positioned at @@ -627,7 +629,7 @@ void main() { // still being investigated. // TODO(cyanglaz): Remove this temporary fix once the Maps SDK issue is resolved. // https://github.com/flutter/flutter/issues/27550 - await Future.delayed(const Duration(seconds: 3)); + await tester.pumpAndSettle(const Duration(seconds: 3)); final LatLngBounds visibleRegion = await controller.getVisibleRegion(); final LatLng northWest = LatLng( @@ -636,11 +638,10 @@ void main() { ); final ScreenCoordinate topLeft = await controller.getScreenCoordinate(northWest); - expect(topLeft, const ScreenCoordinate(x: 0, y: 0)); }); - test('testResizeWidget', () async { + testWidgets('testResizeWidget', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final GoogleMap map = GoogleMap( @@ -649,14 +650,14 @@ void main() { controllerCompleter.complete(controller); }, ); - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: MaterialApp( home: Scaffold( body: SizedBox(height: 100, width: 100, child: map))))); final GoogleMapController controller = await controllerCompleter.future; - await pumpWidget(Directionality( + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: MaterialApp( home: Scaffold( @@ -668,7 +669,7 @@ void main() { // still being investigated. // TODO(cyanglaz): Remove this temporary fix once the Maps SDK issue is resolved. // https://github.com/flutter/flutter/issues/27550 - await Future.delayed(const Duration(seconds: 3)); + await tester.pumpAndSettle(const Duration(seconds: 3)); // Simple call to make sure that the app hasn't crashed. final LatLngBounds bounds1 = await controller.getVisibleRegion(); diff --git a/packages/shared_preferences/example/test_driver/shared_preferences_e2e_test.dart b/packages/google_maps_flutter/example/test_driver/google_maps_e2e_test.dart similarity index 100% rename from packages/shared_preferences/example/test_driver/shared_preferences_e2e_test.dart rename to packages/google_maps_flutter/example/test_driver/google_maps_e2e_test.dart diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapCircleController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapCircleController.m index 92e951200437..6688d4d57695 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapCircleController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapCircleController.m @@ -73,17 +73,17 @@ static CLLocationDistance ToDistance(NSNumber* data) { static void InterpretCircleOptions(NSDictionary* data, id sink, NSObject* registrar) { NSNumber* consumeTapEvents = data[@"consumeTapEvents"]; - if (consumeTapEvents) { + if (consumeTapEvents != nil) { [sink setConsumeTapEvents:ToBool(consumeTapEvents)]; } NSNumber* visible = data[@"visible"]; - if (visible) { + if (visible != nil) { [sink setVisible:ToBool(visible)]; } NSNumber* zIndex = data[@"zIndex"]; - if (zIndex) { + if (zIndex != nil) { [sink setZIndex:ToInt(zIndex)]; } @@ -93,22 +93,22 @@ static void InterpretCircleOptions(NSDictionary* data, id - (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args + arguments:(nullable id)args registrar:(NSObject *)registrar; - (void)showAtX:(CGFloat)x Y:(CGFloat)y; - (void)hide; - (void)animateWithCameraUpdate:(GMSCameraUpdate *)cameraUpdate; - (void)moveWithCameraUpdate:(GMSCameraUpdate *)cameraUpdate; -- (GMSCameraPosition *)cameraPosition; +- (nullable GMSCameraPosition *)cameraPosition; @end // Allows the engine to create new Google Map instances. @@ -49,4 +49,4 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithRegistrar:(NSObject *)registrar; @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m index 70a278af45de..ce85887d5d97 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -65,7 +65,7 @@ - (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args registrar:(NSObject*)registrar { - if ([super init]) { + if (self = [super init]) { _viewId = viewId; GMSCameraPosition* camera = ToOptionalCameraPosition(args[@"initialCameraPosition"]); @@ -543,7 +543,7 @@ static void InterpretMapOptions(NSDictionary* data, id [sink setCameraTargetBounds:ToOptionalBounds(cameraTargetBounds)]; } NSNumber* compassEnabled = data[@"compassEnabled"]; - if (compassEnabled) { + if (compassEnabled != nil) { [sink setCompassEnabled:ToBool(compassEnabled)]; } id indoorEnabled = data[@"indoorEnabled"]; @@ -574,31 +574,31 @@ static void InterpretMapOptions(NSDictionary* data, id } NSNumber* rotateGesturesEnabled = data[@"rotateGesturesEnabled"]; - if (rotateGesturesEnabled) { + if (rotateGesturesEnabled != nil) { [sink setRotateGesturesEnabled:ToBool(rotateGesturesEnabled)]; } NSNumber* scrollGesturesEnabled = data[@"scrollGesturesEnabled"]; - if (scrollGesturesEnabled) { + if (scrollGesturesEnabled != nil) { [sink setScrollGesturesEnabled:ToBool(scrollGesturesEnabled)]; } NSNumber* tiltGesturesEnabled = data[@"tiltGesturesEnabled"]; - if (tiltGesturesEnabled) { + if (tiltGesturesEnabled != nil) { [sink setTiltGesturesEnabled:ToBool(tiltGesturesEnabled)]; } NSNumber* trackCameraPosition = data[@"trackCameraPosition"]; - if (trackCameraPosition) { + if (trackCameraPosition != nil) { [sink setTrackCameraPosition:ToBool(trackCameraPosition)]; } NSNumber* zoomGesturesEnabled = data[@"zoomGesturesEnabled"]; - if (zoomGesturesEnabled) { + if (zoomGesturesEnabled != nil) { [sink setZoomGesturesEnabled:ToBool(zoomGesturesEnabled)]; } NSNumber* myLocationEnabled = data[@"myLocationEnabled"]; - if (myLocationEnabled) { + if (myLocationEnabled != nil) { [sink setMyLocationEnabled:ToBool(myLocationEnabled)]; } NSNumber* myLocationButtonEnabled = data[@"myLocationButtonEnabled"]; - if (myLocationButtonEnabled) { + if (myLocationButtonEnabled != nil) { [sink setMyLocationButtonEnabled:ToBool(myLocationButtonEnabled)]; } } diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m index 2f0d4a989b9d..76a420f993db 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m @@ -95,7 +95,7 @@ static CLLocationCoordinate2D ToLocation(NSArray* data) { static void InterpretMarkerOptions(NSDictionary* data, id sink, NSObject* registrar) { NSNumber* alpha = data[@"alpha"]; - if (alpha) { + if (alpha != nil) { [sink setAlpha:ToFloat(alpha)]; } NSArray* anchor = data[@"anchor"]; @@ -103,7 +103,7 @@ static void InterpretMarkerOptions(NSDictionary* data, id sink, NSObject* registrar) { NSNumber* consumeTapEvents = data[@"consumeTapEvents"]; - if (consumeTapEvents) { + if (consumeTapEvents != nil) { [sink setConsumeTapEvents:ToBool(consumeTapEvents)]; } NSNumber* visible = data[@"visible"]; - if (visible) { + if (visible != nil) { [sink setVisible:ToBool(visible)]; } NSNumber* zIndex = data[@"zIndex"]; - if (zIndex) { + if (zIndex != nil) { [sink setZIndex:ToInt(zIndex)]; } @@ -90,17 +90,17 @@ static void InterpretPolygonOptions(NSDictionary* data, id sink, NSObject* registrar) { NSNumber* consumeTapEvents = data[@"consumeTapEvents"]; - if (consumeTapEvents) { + if (consumeTapEvents != nil) { [sink setConsumeTapEvents:ToBool(consumeTapEvents)]; } NSNumber* visible = data[@"visible"]; - if (visible) { + if (visible != nil) { [sink setVisible:ToBool(visible)]; } NSNumber* zIndex = data[@"zIndex"]; - if (zIndex) { + if (zIndex != nil) { [sink setZIndex:ToInt(zIndex)]; } @@ -87,12 +87,12 @@ static void InterpretPolylineOptions(NSDictionary* data, id 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } end diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index 3e8b80283e6e..320e942bf208 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter -version: 0.5.21+9 +version: 0.5.21+12 dependencies: flutter: diff --git a/packages/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md similarity index 95% rename from packages/google_sign_in/CHANGELOG.md rename to packages/google_sign_in/google_sign_in/CHANGELOG.md index 300eba20968e..3ea1a59c38d3 100644 --- a/packages/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,15 @@ +## 4.0.14 + +* Port plugin code to use the federated Platform Interface, instead of a MethodChannel directly. + +## 4.0.13 + +* Fix `GoogleUserCircleAvatar` to handle new style profile image URLs. + +## 4.0.12 + +* Move google_sign_in plugin to google_sign_in/google_sign_in to prepare for federated implementations. + ## 4.0.11 * Update iOS CocoaPod dependency to 5.0 to fix deprecated API usage issue. diff --git a/packages/google_sign_in/LICENSE b/packages/google_sign_in/google_sign_in/LICENSE similarity index 100% rename from packages/google_sign_in/LICENSE rename to packages/google_sign_in/google_sign_in/LICENSE diff --git a/packages/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md similarity index 95% rename from packages/google_sign_in/README.md rename to packages/google_sign_in/google_sign_in/README.md index 01c8b5275365..f9e8415f0745 100755 --- a/packages/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -87,11 +87,11 @@ Future _handleSignIn() async { ## Example -Find the example wiring in the [Google sign-in example application](https://github.com/flutter/plugins/blob/master/packages/google_sign_in/example/lib/main.dart). +Find the example wiring in the [Google sign-in example application](https://github.com/flutter/plugins/blob/master/packages/google_sign_in/google_sign_in/example/lib/main.dart). ## API details -See the [google_sign_in.dart](https://github.com/flutter/plugins/blob/master/packages/google_sign_in/lib/google_sign_in.dart) for more API details. +See the [google_sign_in.dart](https://github.com/flutter/plugins/blob/master/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart) for more API details. ## Issues and feedback diff --git a/packages/google_sign_in/google_sign_in/analysis_options.yaml b/packages/google_sign_in/google_sign_in/analysis_options.yaml new file mode 100644 index 000000000000..e1dadb9e2b2a --- /dev/null +++ b/packages/google_sign_in/google_sign_in/analysis_options.yaml @@ -0,0 +1,11 @@ +# This is a temporary file to allow us to land a new set of linter rules in a +# series of manageable patches instead of one gigantic PR. It disables some of +# the new lints that are already failing on this plugin, for this plugin. It +# should be deleted and the failing lints addressed as soon as possible. + +include: ../../../analysis_options.yaml + +analyzer: + errors: + public_member_api_docs: ignore + unawaited_futures: ignore diff --git a/packages/google_sign_in/android/build.gradle b/packages/google_sign_in/google_sign_in/android/build.gradle similarity index 100% rename from packages/google_sign_in/android/build.gradle rename to packages/google_sign_in/google_sign_in/android/build.gradle diff --git a/packages/google_sign_in/android/gradle.properties b/packages/google_sign_in/google_sign_in/android/gradle.properties similarity index 100% rename from packages/google_sign_in/android/gradle.properties rename to packages/google_sign_in/google_sign_in/android/gradle.properties diff --git a/packages/google_sign_in/android/settings.gradle b/packages/google_sign_in/google_sign_in/android/settings.gradle similarity index 100% rename from packages/google_sign_in/android/settings.gradle rename to packages/google_sign_in/google_sign_in/android/settings.gradle diff --git a/packages/google_sign_in/android/src/main/AndroidManifest.xml b/packages/google_sign_in/google_sign_in/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/google_sign_in/android/src/main/AndroidManifest.xml rename to packages/google_sign_in/google_sign_in/android/src/main/AndroidManifest.xml diff --git a/packages/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java b/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java similarity index 100% rename from packages/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java rename to packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java diff --git a/packages/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/Executors.java b/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/Executors.java similarity index 100% rename from packages/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/Executors.java rename to packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/Executors.java diff --git a/packages/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java similarity index 100% rename from packages/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java rename to packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java diff --git a/packages/google_sign_in/example/README.md b/packages/google_sign_in/google_sign_in/example/README.md similarity index 100% rename from packages/google_sign_in/example/README.md rename to packages/google_sign_in/google_sign_in/example/README.md diff --git a/packages/google_sign_in/example/android.iml b/packages/google_sign_in/google_sign_in/example/android.iml similarity index 100% rename from packages/google_sign_in/example/android.iml rename to packages/google_sign_in/google_sign_in/example/android.iml diff --git a/packages/google_sign_in/example/android/app/build.gradle b/packages/google_sign_in/google_sign_in/example/android/app/build.gradle similarity index 100% rename from packages/google_sign_in/example/android/app/build.gradle rename to packages/google_sign_in/google_sign_in/example/android/app/build.gradle diff --git a/packages/google_sign_in/example/android/app/google-services.json b/packages/google_sign_in/google_sign_in/example/android/app/google-services.json similarity index 100% rename from packages/google_sign_in/example/android/app/google-services.json rename to packages/google_sign_in/google_sign_in/example/android/app/google-services.json diff --git a/packages/google_sign_in/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/google_sign_in/google_sign_in/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/google_sign_in/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/google_sign_in/google_sign_in/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/google_sign_in/example/android/app/src/main/AndroidManifest.xml b/packages/google_sign_in/google_sign_in/example/android/app/src/main/AndroidManifest.xml similarity index 100% rename from packages/google_sign_in/example/android/app/src/main/AndroidManifest.xml rename to packages/google_sign_in/google_sign_in/example/android/app/src/main/AndroidManifest.xml diff --git a/packages/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/.gitignore b/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/.gitignore similarity index 100% rename from packages/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/.gitignore rename to packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/.gitignore diff --git a/packages/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/MainActivity.java b/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/MainActivity.java similarity index 100% rename from packages/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/MainActivity.java rename to packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/MainActivity.java diff --git a/packages/google_sign_in/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/google_sign_in/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/google_sign_in/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/google_sign_in/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/google_sign_in/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/google_sign_in/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/google_sign_in/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/google_sign_in/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/google_sign_in/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/google_sign_in/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/google_sign_in/example/android/build.gradle b/packages/google_sign_in/google_sign_in/example/android/build.gradle similarity index 100% rename from packages/google_sign_in/example/android/build.gradle rename to packages/google_sign_in/google_sign_in/example/android/build.gradle diff --git a/packages/google_sign_in/example/android/gradle.properties b/packages/google_sign_in/google_sign_in/example/android/gradle.properties similarity index 100% rename from packages/google_sign_in/example/android/gradle.properties rename to packages/google_sign_in/google_sign_in/example/android/gradle.properties diff --git a/packages/google_sign_in/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/google_sign_in/google_sign_in/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/google_sign_in/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/google_sign_in/google_sign_in/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/google_sign_in/example/android/settings.gradle b/packages/google_sign_in/google_sign_in/example/android/settings.gradle similarity index 100% rename from packages/google_sign_in/example/android/settings.gradle rename to packages/google_sign_in/google_sign_in/example/android/settings.gradle diff --git a/packages/google_sign_in/example/example.iml b/packages/google_sign_in/google_sign_in/example/example.iml similarity index 100% rename from packages/google_sign_in/example/example.iml rename to packages/google_sign_in/google_sign_in/example/example.iml diff --git a/packages/google_sign_in/example/google_sign_in_example.iml b/packages/google_sign_in/google_sign_in/example/google_sign_in_example.iml similarity index 100% rename from packages/google_sign_in/example/google_sign_in_example.iml rename to packages/google_sign_in/google_sign_in/example/google_sign_in_example.iml diff --git a/packages/google_sign_in/example/ios/Flutter/AppFrameworkInfo.plist b/packages/google_sign_in/google_sign_in/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from packages/google_sign_in/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/google_sign_in/google_sign_in/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/packages/google_sign_in/example/ios/Flutter/Debug.xcconfig b/packages/google_sign_in/google_sign_in/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/google_sign_in/example/ios/Flutter/Debug.xcconfig rename to packages/google_sign_in/google_sign_in/example/ios/Flutter/Debug.xcconfig diff --git a/packages/google_sign_in/example/ios/Flutter/Release.xcconfig b/packages/google_sign_in/google_sign_in/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/google_sign_in/example/ios/Flutter/Release.xcconfig rename to packages/google_sign_in/google_sign_in/example/ios/Flutter/Release.xcconfig diff --git a/packages/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from packages/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj rename to packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj diff --git a/packages/google_sign_in/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/google_sign_in/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/packages/google_sign_in/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from packages/google_sign_in/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/packages/google_sign_in/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/google_sign_in/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/google_sign_in/google_sign_in/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/google_sign_in/example/ios/Runner/AppDelegate.h b/packages/google_sign_in/google_sign_in/example/ios/Runner/AppDelegate.h similarity index 100% rename from packages/google_sign_in/example/ios/Runner/AppDelegate.h rename to packages/google_sign_in/google_sign_in/example/ios/Runner/AppDelegate.h diff --git a/packages/google_sign_in/example/ios/Runner/AppDelegate.m b/packages/google_sign_in/google_sign_in/example/ios/Runner/AppDelegate.m similarity index 100% rename from packages/google_sign_in/example/ios/Runner/AppDelegate.m rename to packages/google_sign_in/google_sign_in/example/ios/Runner/AppDelegate.m diff --git a/packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/google_sign_in/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/google_sign_in/google_sign_in/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/google_sign_in/example/ios/Runner/Base.lproj/Main.storyboard b/packages/google_sign_in/google_sign_in/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/google_sign_in/example/ios/Runner/GoogleService-Info.plist b/packages/google_sign_in/google_sign_in/example/ios/Runner/GoogleService-Info.plist similarity index 100% rename from packages/google_sign_in/example/ios/Runner/GoogleService-Info.plist rename to packages/google_sign_in/google_sign_in/example/ios/Runner/GoogleService-Info.plist diff --git a/packages/google_sign_in/example/ios/Runner/Info.plist b/packages/google_sign_in/google_sign_in/example/ios/Runner/Info.plist similarity index 100% rename from packages/google_sign_in/example/ios/Runner/Info.plist rename to packages/google_sign_in/google_sign_in/example/ios/Runner/Info.plist diff --git a/packages/google_sign_in/example/ios/Runner/main.m b/packages/google_sign_in/google_sign_in/example/ios/Runner/main.m similarity index 100% rename from packages/google_sign_in/example/ios/Runner/main.m rename to packages/google_sign_in/google_sign_in/example/ios/Runner/main.m diff --git a/packages/google_sign_in/example/lib/main.dart b/packages/google_sign_in/google_sign_in/example/lib/main.dart similarity index 100% rename from packages/google_sign_in/example/lib/main.dart rename to packages/google_sign_in/google_sign_in/example/lib/main.dart diff --git a/packages/google_sign_in/example/pubspec.yaml b/packages/google_sign_in/google_sign_in/example/pubspec.yaml similarity index 100% rename from packages/google_sign_in/example/pubspec.yaml rename to packages/google_sign_in/google_sign_in/example/pubspec.yaml diff --git a/packages/google_sign_in/ios/Assets/.gitkeep b/packages/google_sign_in/google_sign_in/ios/Assets/.gitkeep similarity index 100% rename from packages/google_sign_in/ios/Assets/.gitkeep rename to packages/google_sign_in/google_sign_in/ios/Assets/.gitkeep diff --git a/packages/google_sign_in/ios/Classes/GoogleSignInPlugin.h b/packages/google_sign_in/google_sign_in/ios/Classes/GoogleSignInPlugin.h similarity index 100% rename from packages/google_sign_in/ios/Classes/GoogleSignInPlugin.h rename to packages/google_sign_in/google_sign_in/ios/Classes/GoogleSignInPlugin.h diff --git a/packages/google_sign_in/ios/Classes/GoogleSignInPlugin.m b/packages/google_sign_in/google_sign_in/ios/Classes/GoogleSignInPlugin.m similarity index 100% rename from packages/google_sign_in/ios/Classes/GoogleSignInPlugin.m rename to packages/google_sign_in/google_sign_in/ios/Classes/GoogleSignInPlugin.m diff --git a/packages/google_sign_in/ios/google_sign_in.podspec b/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec similarity index 100% rename from packages/google_sign_in/ios/google_sign_in.podspec rename to packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec diff --git a/packages/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart similarity index 76% rename from packages/google_sign_in/lib/google_sign_in.dart rename to packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index f1e1db21801e..7556c1006c2f 100644 --- a/packages/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -5,38 +5,36 @@ import 'dart:async'; import 'dart:ui' show hashValues; -import 'package:flutter/services.dart' show MethodChannel, PlatformException; -import 'package:meta/meta.dart' show visibleForTesting; +import 'package:flutter/services.dart' show PlatformException; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'src/common.dart'; export 'src/common.dart'; export 'widgets.dart'; -enum SignInOption { standard, games } - class GoogleSignInAuthentication { GoogleSignInAuthentication._(this._data); - final Map _data; + final GoogleSignInTokenData _data; /// An OpenID Connect ID token that identifies the user. - String get idToken => _data['idToken']; + String get idToken => _data.idToken; /// The OAuth2 access token to access Google services. - String get accessToken => _data['accessToken']; + String get accessToken => _data.accessToken; @override String toString() => 'GoogleSignInAuthentication:$_data'; } class GoogleSignInAccount implements GoogleIdentity { - GoogleSignInAccount._(this._googleSignIn, Map data) - : displayName = data['displayName'], - email = data['email'], - id = data['id'], - photoUrl = data['photoUrl'], - _idToken = data['idToken'] { + GoogleSignInAccount._(this._googleSignIn, GoogleSignInUserData data) + : displayName = data.displayName, + email = data.email, + id = data.id, + photoUrl = data.photoUrl, + _idToken = data.idToken { assert(id != null); } @@ -78,18 +76,16 @@ class GoogleSignInAccount implements GoogleIdentity { throw StateError('User is no longer signed in.'); } - final Map response = - await GoogleSignIn.channel.invokeMapMethod( - 'getTokens', - { - 'email': email, - 'shouldRecoverAuth': true, - }, + final GoogleSignInTokenData response = + await GoogleSignInPlatform.instance.getTokens( + email: email, + shouldRecoverAuth: true, ); + // On Android, there isn't an API for refreshing the idToken, so re-use // the one we obtained on login. - if (response['idToken'] == null) { - response['idToken'] = _idToken; + if (response.idToken == null) { + response.idToken = _idToken; } return GoogleSignInAuthentication._(response); } @@ -108,10 +104,7 @@ class GoogleSignInAccount implements GoogleIdentity { /// this method and grab `authHeaders` once again. Future clearAuthCache() async { final String token = (await authentication).accessToken; - await GoogleSignIn.channel.invokeMethod( - 'clearAuthCache', - {'token': token}, - ); + await GoogleSignInPlatform.instance.clearAuthCache(token: token); } @override @@ -146,7 +139,7 @@ class GoogleSignIn { /// Initializes global sign-in configuration settings. /// /// The [signInOption] determines the user experience. [SigninOption.games] - /// must not be used on iOS. + /// is only supported on Android. /// /// The list of [scopes] are OAuth scope codes to request when signing in. /// These scope codes will determine the level of data access that is granted @@ -157,18 +150,25 @@ class GoogleSignIn { /// The [hostedDomain] argument specifies a hosted domain restriction. By /// setting this, sign in will be restricted to accounts of the user in the /// specified domain. By default, the list of accounts will not be restricted. - GoogleSignIn({this.signInOption, this.scopes, this.hostedDomain}); + GoogleSignIn({ + this.signInOption = SignInOption.standard, + this.scopes = const [], + this.hostedDomain, + }); /// Factory for creating default sign in user experience. - factory GoogleSignIn.standard({List scopes, String hostedDomain}) { + factory GoogleSignIn.standard({ + List scopes = const [], + String hostedDomain, + }) { return GoogleSignIn( signInOption: SignInOption.standard, scopes: scopes, hostedDomain: hostedDomain); } - /// Factory for creating sign in suitable for games. This option must not be - /// used on iOS because the games API is not supported. + /// Factory for creating sign in suitable for games. This option is only + /// supported on Android. factory GoogleSignIn.games() { return GoogleSignIn(signInOption: SignInOption.games); } @@ -186,13 +186,8 @@ class GoogleSignIn { /// Error code indicating that attempt to sign in failed. static const String kSignInFailedError = 'sign_in_failed'; - /// The [MethodChannel] over which this class communicates. - @visibleForTesting - static const MethodChannel channel = - MethodChannel('plugins.flutter.io/google_sign_in'); - - /// Option to determine the sign in user experience. [SignInOption.games] must - /// not be used on iOS. + /// Option to determine the sign in user experience. [SignInOption.games] is + /// only supported on Android. final SignInOption signInOption; /// The list of [scopes] are OAuth scope codes requested when signing in. @@ -211,12 +206,12 @@ class GoogleSignIn { // Future that completes when we've finished calling `init` on the native side Future _initialization; - Future _callMethod(String method) async { + Future _callMethod(Function method) async { await _ensureInitialized(); - final Map response = - await channel.invokeMapMethod(method); - return _setCurrentUser(response != null && response.isNotEmpty + final dynamic response = await method(); + + return _setCurrentUser(response != null && response is GoogleSignInUserData ? GoogleSignInAccount._(this, response) : null); } @@ -230,16 +225,14 @@ class GoogleSignIn { } Future _ensureInitialized() { - return _initialization ??= - channel.invokeMethod('init', { - 'signInOption': (signInOption ?? SignInOption.standard).toString(), - 'scopes': scopes ?? [], - 'hostedDomain': hostedDomain, - }) - ..catchError((dynamic _) { - // Invalidate initialization if it errored out. - _initialization = null; - }); + return _initialization ??= GoogleSignInPlatform.instance.init( + signInOption: signInOption, + scopes: scopes, + hostedDomain: hostedDomain, + )..catchError((dynamic _) { + // Invalidate initialization if it errors out. + _initialization = null; + }); } /// The most recently scheduled method call. @@ -251,6 +244,7 @@ class GoogleSignIn { final Completer completer = Completer(); future.whenComplete(completer.complete).catchError((dynamic _) { // Ignore if previous call completed with an error. + // TODO: Should we log errors here, if debug or similar? }); return completer.future; } @@ -259,26 +253,29 @@ class GoogleSignIn { /// /// At most one in flight call is allowed to prevent concurrent (out of order) /// updates to [currentUser] and [onCurrentUserChanged]. - Future _addMethodCall(String method) async { + /// + /// The optional, named parameter [canSkipCall] lets the plugin know that the + /// method call may be skipped, if there's already [_currentUser] information. + /// This is used from the [signIn] and [signInSilently] methods. + Future _addMethodCall( + Function method, { + bool canSkipCall = false, + }) async { Future response; if (_lastMethodCall == null) { response = _callMethod(method); } else { response = _lastMethodCall.then((_) { // If after the last completed call `currentUser` is not `null` and requested - // method is a sign in method, re-use the same authenticated user + // method can be skipped (`canSkipCall`), re-use the same authenticated user // instead of making extra call to the native side. - const List kSignInMethods = [ - 'signIn', - 'signInSilently' - ]; - if (kSignInMethods.contains(method) && _currentUser != null) { + if (canSkipCall && _currentUser != null) { return _currentUser; - } else { - return _callMethod(method); } + return _callMethod(method); }); } + // Add the current response to the currently running Promise of all pending responses _lastMethodCall = _waitFor(response); return response; } @@ -303,10 +300,12 @@ class GoogleSignIn { /// returned Future completes with [PlatformException] whose `code` can be /// either [kSignInRequiredError] (when there is no authenticated user) or /// [kSignInFailedError] (when an unknown error occurred). - Future signInSilently( - {bool suppressErrors = true}) async { + Future signInSilently({ + bool suppressErrors = true, + }) async { try { - return await _addMethodCall('signInSilently'); + return await _addMethodCall(GoogleSignInPlatform.instance.signInSilently, + canSkipCall: true); } catch (_) { if (suppressErrors) { return null; @@ -319,7 +318,7 @@ class GoogleSignIn { /// Returns a future that resolves to whether a user is currently signed in. Future isSignedIn() async { await _ensureInitialized(); - return await channel.invokeMethod('isSignedIn'); + return GoogleSignInPlatform.instance.isSignedIn(); } /// Starts the interactive sign-in process. @@ -333,16 +332,19 @@ class GoogleSignIn { /// /// Re-authentication can be triggered only after [signOut] or [disconnect]. Future signIn() { - final Future result = _addMethodCall('signIn'); + final Future result = + _addMethodCall(GoogleSignInPlatform.instance.signIn, canSkipCall: true); bool isCanceled(dynamic error) => error is PlatformException && error.code == kSignInCanceledError; return result.catchError((dynamic _) => null, test: isCanceled); } /// Marks current user as being in the signed out state. - Future signOut() => _addMethodCall('signOut'); + Future signOut() => + _addMethodCall(GoogleSignInPlatform.instance.signOut); /// Disconnects the current user from the app and revokes previous /// authentication. - Future disconnect() => _addMethodCall('disconnect'); + Future disconnect() => + _addMethodCall(GoogleSignInPlatform.instance.disconnect); } diff --git a/packages/google_sign_in/lib/src/common.dart b/packages/google_sign_in/google_sign_in/lib/src/common.dart similarity index 100% rename from packages/google_sign_in/lib/src/common.dart rename to packages/google_sign_in/google_sign_in/lib/src/common.dart diff --git a/packages/google_sign_in/google_sign_in/lib/src/fife.dart b/packages/google_sign_in/google_sign_in/lib/src/fife.dart new file mode 100644 index 000000000000..14ecf5fd6083 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/lib/src/fife.dart @@ -0,0 +1,70 @@ +// 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. + +/// A regular expression that matches against the "size directive" path +/// segment of Google profile image URLs. +/// +/// The format is is "`/sNN-c/`", where `NN` is the max width/height of the +/// image, and "`c`" indicates we want the image cropped. +final RegExp sizeDirective = RegExp(r'^s[0-9]{1,5}(-c)?$'); + +/// Adds [size] (and crop) directive to [photoUrl]. +/// +/// There are two formats for photoUrls coming from the Sign In backend. +/// +/// The two formats can be told apart by the number of path segments in the +/// URL (path segments: parts of the URL separated by slashes "/"): +/// +/// * If the URL has 2 or less path segments, it is a *new* style URL. +/// * If the URL has more than 2 path segments, it is an old style URL. +/// +/// Old style URLs encode the image transformation directives as the last +/// path segment. Look at the [sizeDirective] Regular Expression for more +/// information about these URLs. +/// +/// New style URLs carry the same directives at the end of the URL, +/// after an = sign, like: "`=s120-c-fSoften=1,50,0`". +/// +/// Directives may contain the "=" sign (`fSoften=1,50,0`), but it seems the +/// base URL of the images don't. "Everything after the first = sign" is a +/// good heuristic to split new style URLs. +/// +/// Each directive is separated from others by dashes. Directives are the same +/// as described in the [sizeDirective] RegExp. +/// +/// Modified image URLs are recomposed by performing the parsing steps in reverse. +String addSizeDirectiveToUrl(String photoUrl, double size) { + final Uri profileUri = Uri.parse(photoUrl); + final List pathSegments = List.from(profileUri.pathSegments); + if (pathSegments.length <= 2) { + final String imagePath = pathSegments.last; + // Does this have any existing transformation directives? + final int directiveSeparator = imagePath.indexOf('='); + if (directiveSeparator >= 0) { + // Split the baseUrl from the sizing directive by the first "=" + final String baseUrl = imagePath.substring(0, directiveSeparator); + final String directive = imagePath.substring(directiveSeparator + 1); + // Split the directive by "-" + final Set directives = Set.from(directive.split('-')) + // Remove the size directive, if present, and any empty values + ..removeWhere((String s) => s.isEmpty || sizeDirective.hasMatch(s)) + // Add the size and crop directives + ..addAll(['c', 's${size.round()}']); + // Recompose the URL by performing the reverse of the parsing + pathSegments.last = '$baseUrl=${directives.join("-")}'; + } else { + pathSegments.last = '${pathSegments.last}=c-s${size.round()}'; + } + } else { + // Old style URLs + pathSegments + ..removeWhere(sizeDirective.hasMatch) + ..insert(pathSegments.length - 1, 's${size.round()}-c'); + } + return Uri( + scheme: profileUri.scheme, + host: profileUri.host, + pathSegments: pathSegments, + ).toString(); +} diff --git a/packages/google_sign_in/lib/testing.dart b/packages/google_sign_in/google_sign_in/lib/testing.dart similarity index 100% rename from packages/google_sign_in/lib/testing.dart rename to packages/google_sign_in/google_sign_in/lib/testing.dart diff --git a/packages/google_sign_in/lib/widgets.dart b/packages/google_sign_in/google_sign_in/lib/widgets.dart similarity index 88% rename from packages/google_sign_in/lib/widgets.dart rename to packages/google_sign_in/google_sign_in/lib/widgets.dart index 01ab6c64c00c..3375628f47b5 100644 --- a/packages/google_sign_in/lib/widgets.dart +++ b/packages/google_sign_in/google_sign_in/lib/widgets.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'src/common.dart'; +import 'src/fife.dart' as fife; /// Builds a CircleAvatar profile image of the appropriate resolution class GoogleUserCircleAvatar extends StatelessWidget { @@ -33,7 +34,7 @@ class GoogleUserCircleAvatar extends StatelessWidget { /// /// The format is is "`/sNN-c/`", where `NN` is the max width/height of the /// image, and "`c`" indicates we want the image cropped. - static final RegExp sizeDirective = RegExp(r'^s[0-9]{1,5}(-c)?$'); + static final RegExp sizeDirective = fife.sizeDirective; /// The Google user's identity; guaranteed to be non-null. final GoogleIdentity identity; @@ -67,8 +68,7 @@ class GoogleUserCircleAvatar extends StatelessWidget { ); } - /// Adds sizing information to [photoUrl], inserted as the last path segment - /// before the image filename. The format is described in [sizeDirective]. + /// Adds correct sizing information to [photoUrl]. /// /// Falls back to the default profile photo if [photoUrl] is [null]. static String _sizedProfileImageUrl(String photoUrl, double size) { @@ -77,17 +77,7 @@ class GoogleUserCircleAvatar extends StatelessWidget { // the default profile photo as a last resort. return 'https://lh3.googleusercontent.com/a/default-user=s${size.round()}-c'; } - final Uri profileUri = Uri.parse(photoUrl); - final List pathSegments = - List.from(profileUri.pathSegments); - pathSegments - ..removeWhere(sizeDirective.hasMatch) - ..insert(pathSegments.length - 1, 's${size.round()}-c'); - return Uri( - scheme: profileUri.scheme, - host: profileUri.host, - pathSegments: pathSegments, - ).toString(); + return fife.addSizeDirectiveToUrl(photoUrl, size); } Widget _buildClippedImage(BuildContext context, BoxConstraints constraints) { diff --git a/packages/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml similarity index 86% rename from packages/google_sign_in/pubspec.yaml rename to packages/google_sign_in/google_sign_in/pubspec.yaml index e239e01c3b44..30e2b88016bf 100644 --- a/packages/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -2,8 +2,8 @@ name: google_sign_in description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android and iOS. author: Flutter Team -homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in -version: 4.0.11 +homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in +version: 4.0.14 flutter: plugin: @@ -12,6 +12,7 @@ flutter: pluginClass: GoogleSignInPlugin dependencies: + google_sign_in_platform_interface: ^1.0.0 flutter: sdk: flutter meta: ^1.0.4 diff --git a/packages/google_sign_in/google_sign_in/test/fife_test.dart b/packages/google_sign_in/google_sign_in/test/fife_test.dart new file mode 100644 index 000000000000..bfc4937a7c64 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/test/fife_test.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 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in/src/fife.dart'; + +void main() { + group('addSizeDirectiveToUrl', () { + const double size = 20; + + group('Old style URLs', () { + const String base = + 'https://lh3.googleusercontent.com/-ukEAtRyRhw8/AAAAAAAAAAI/AAAAAAAAAAA/ACHi3rfhID9XACtdb9q_xK43VSXQvBV11Q.CMID'; + const String expected = '$base/s20-c/photo.jpg'; + + test('with directives, sets size', () { + final String url = '$base/s64-c/photo.jpg'; + expect(addSizeDirectiveToUrl(url, size), expected); + }); + + test('no directives, sets size and crop', () { + final String url = '$base/photo.jpg'; + expect(addSizeDirectiveToUrl(url, size), expected); + }); + + test('no crop, sets size and crop', () { + final String url = '$base/s64/photo.jpg'; + expect(addSizeDirectiveToUrl(url, size), expected); + }); + }); + + group('New style URLs', () { + const String base = + 'https://lh3.googleusercontent.com/a-/AAuE7mC0Lh4F4uDtEaY7hpe-GIsbDpqfMZ3_2UhBQ8Qk'; + const String expected = '$base=c-s20'; + + test('with directives, sets size', () { + final String url = '$base=s120-c'; + expect(addSizeDirectiveToUrl(url, size), expected); + }); + + test('no directives, sets size and crop', () { + final String url = base; + expect(addSizeDirectiveToUrl(url, size), expected); + }); + + test('no directives, but with an equals sign, sets size and crop', () { + final String url = '$base='; + expect(addSizeDirectiveToUrl(url, size), expected); + }); + + test('no crop, adds crop', () { + final String url = '$base=s120'; + expect(addSizeDirectiveToUrl(url, size), expected); + }); + + test('many directives, sets size and crop, preserves other directives', + () { + final String url = '$base=s120-c-fSoften=1,50,0'; + final String expected = '$base=c-fSoften=1,50,0-s20'; + expect(addSizeDirectiveToUrl(url, size), expected); + }); + }); + }); +} diff --git a/packages/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart similarity index 98% rename from packages/google_sign_in/test/google_sign_in_test.dart rename to packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index 108edf9c892b..a85fb0f27e42 100755 --- a/packages/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:google_sign_in/testing.dart'; @@ -391,7 +392,9 @@ void main() { GoogleSignIn googleSignIn; setUp(() { - GoogleSignIn.channel.setMockMethodCallHandler( + final MethodChannelGoogleSignIn platformInstance = + GoogleSignInPlatform.instance; + platformInstance.channel.setMockMethodCallHandler( (FakeSignInBackend()..user = kUserData).handleMethodCall); googleSignIn = GoogleSignIn(); }); diff --git a/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md b/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..343608d89d3e --- /dev/null +++ b/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md @@ -0,0 +1,7 @@ +## 1.0.1 + +* Switch away from quiver_hashcode. + +## 1.0.0 + +* Initial release. diff --git a/packages/video_player/LICENSE b/packages/google_sign_in/google_sign_in_platform_interface/LICENSE similarity index 100% rename from packages/video_player/LICENSE rename to packages/google_sign_in/google_sign_in_platform_interface/LICENSE diff --git a/packages/google_sign_in/google_sign_in_platform_interface/README.md b/packages/google_sign_in/google_sign_in_platform_interface/README.md new file mode 100644 index 000000000000..9fd891f63968 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_platform_interface/README.md @@ -0,0 +1,26 @@ +# google_sign_in_platform_interface + +A common platform interface for the [`google_sign_in`][1] plugin. + +This interface allows platform-specific implementations of the `google_sign_in` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `google_sign_in`, extend +[`GoogleSignInPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`GoogleSignInPlatform` by calling +`GoogleSignInPlatform.instance = MyPlatformGoogleSignIn()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../google_sign_in +[2]: lib/google_sign_in_platform_interface.dart diff --git a/packages/google_sign_in/google_sign_in_platform_interface/analysis_options.yaml b/packages/google_sign_in/google_sign_in_platform_interface/analysis_options.yaml new file mode 100644 index 000000000000..969b55796944 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_platform_interface/analysis_options.yaml @@ -0,0 +1,10 @@ +# This is a temporary file to allow us to land a new set of linter rules in a +# series of manageable patches instead of one gigantic PR. It disables some of +# the new lints that are already failing on this plugin, for this plugin. It +# should be deleted and the failing lints addressed as soon as possible. + +include: ../../../analysis_options.yaml + +analyzer: + errors: + public_member_api_docs: ignore diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart new file mode 100644 index 000000000000..8e4e4541a388 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart @@ -0,0 +1,109 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'package:meta/meta.dart' show required, visibleForTesting; +import 'src/method_channel_google_sign_in.dart'; +import 'src/types.dart'; + +export 'src/method_channel_google_sign_in.dart'; +export 'src/types.dart'; + +/// The interface that implementations of google_sign_in must implement. +/// +/// Platform implementations that live in a separate package should extend this +/// class rather than implement it as `google_sign_in` does not consider newly +/// added methods to be breaking changes. Extending this class (using `extends`) +/// ensures that the subclass will get the default implementation, while +/// platform implementations that `implements` this interface will be broken by +/// newly added [GoogleSignInPlatform] methods. +abstract class GoogleSignInPlatform { + /// Only mock implementations should set this to `true`. + /// + /// Mockito mocks implement this class with `implements` which is forbidden + /// (see class docs). This property provides a backdoor for mocks to skip the + /// verification that the class isn't implemented with `implements`. + @visibleForTesting + bool get isMock => false; + + /// The default instance of [GoogleSignInPlatform] to use. + /// + /// Platform-specific plugins should override this with their own + /// platform-specific class that extends [GoogleSignInPlatform] when they + /// register themselves. + /// + /// Defaults to [MethodChannelGoogleSignIn]. + static GoogleSignInPlatform get instance => _instance; + + static GoogleSignInPlatform _instance = MethodChannelGoogleSignIn(); + + // TODO(amirh): Extract common platform interface logic. + // https://github.com/flutter/flutter/issues/43368 + static set instance(GoogleSignInPlatform instance) { + if (!instance.isMock) { + try { + instance._verifyProvidesDefaultImplementations(); + } on NoSuchMethodError catch (_) { + throw AssertionError( + 'Platform interfaces must not be implemented with `implements`'); + } + } + _instance = instance; + } + + /// This method ensures that [GoogleSignInPlatform] isn't implemented with `implements`. + /// + /// See class docs for more details on why using `implements` to implement + /// [GoogleSignInPlatform] is forbidden. + /// + /// This private method is called by the [instance] setter, which should fail + /// if the provided instance is a class implemented with `implements`. + void _verifyProvidesDefaultImplementations() {} + + /// Initializes the plugin. You must call this method before calling other methods. + /// See: https://developers.google.com/identity/sign-in/web/reference#gapiauth2initparams + Future init( + {@required String hostedDomain, + List scopes, + SignInOption signInOption, + String clientId}) async { + throw UnimplementedError('init() has not been implemented.'); + } + + /// Attempts to reuse pre-existing credentials to sign in again, without user interaction. + Future signInSilently() async { + throw UnimplementedError('signInSilently() has not been implemented.'); + } + + /// Signs in the user with the options specified to [init]. + Future signIn() async { + throw UnimplementedError('signIn() has not been implemented.'); + } + + /// Returns the Tokens used to authenticate other API calls. + Future getTokens( + {@required String email, bool shouldRecoverAuth}) async { + throw UnimplementedError('getTokens() has not been implemented.'); + } + + /// Signs out the current account from the application. + Future signOut() async { + throw UnimplementedError('signOut() has not been implemented.'); + } + + /// Revokes all of the scopes that the user granted. + Future disconnect() async { + throw UnimplementedError('disconnect() has not been implemented.'); + } + + /// Returns whether the current user is currently signed in. + Future isSignedIn() async { + throw UnimplementedError('isSignedIn() has not been implemented.'); + } + + /// Clears any cached information that the plugin may be holding on to. + Future clearAuthCache({@required String token}) async { + throw UnimplementedError('clearAuthCache() has not been implemented.'); + } +} diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart new file mode 100644 index 000000000000..bc52a73868eb --- /dev/null +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart @@ -0,0 +1,79 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show required, visibleForTesting; + +import '../google_sign_in_platform_interface.dart'; +import 'types.dart'; +import 'utils.dart'; + +/// An implementation of [GoogleSignInPlatform] that uses method channels. +class MethodChannelGoogleSignIn extends GoogleSignInPlatform { + @visibleForTesting + MethodChannel channel = + const MethodChannel('plugins.flutter.io/google_sign_in'); + + @override + Future init( + {@required String hostedDomain, + List scopes = const [], + SignInOption signInOption = SignInOption.standard, + String clientId}) { + return channel.invokeMethod('init', { + 'signInOption': signInOption.toString(), + 'scopes': scopes, + 'hostedDomain': hostedDomain, + }); + } + + @override + Future signInSilently() { + return channel + .invokeMapMethod('signInSilently') + .then(getUserDataFromMap); + } + + @override + Future signIn() { + return channel + .invokeMapMethod('signIn') + .then(getUserDataFromMap); + } + + @override + Future getTokens( + {String email, bool shouldRecoverAuth = true}) { + return channel + .invokeMapMethod('getTokens', { + 'email': email, + 'shouldRecoverAuth': shouldRecoverAuth, + }).then(getTokenDataFromMap); + } + + @override + Future signOut() { + return channel.invokeMapMethod('signOut'); + } + + @override + Future disconnect() { + return channel.invokeMapMethod('disconnect'); + } + + @override + Future isSignedIn() { + return channel.invokeMethod('isSignedIn'); + } + + @override + Future clearAuthCache({String token}) { + return channel.invokeMethod( + 'clearAuthCache', + {'token': token}, + ); + } +} diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart new file mode 100644 index 000000000000..44f0b30de5f8 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart @@ -0,0 +1,51 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:quiver/core.dart'; + +enum SignInOption { standard, games } + +class GoogleSignInUserData { + GoogleSignInUserData( + {this.displayName, this.email, this.id, this.photoUrl, this.idToken}); + String displayName; + String email; + String id; + String photoUrl; + String idToken; + + @override + int get hashCode => + hashObjects([displayName, email, id, photoUrl, idToken]); + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (other is! GoogleSignInUserData) return false; + final GoogleSignInUserData otherUserData = other; + return otherUserData.displayName == displayName && + otherUserData.email == email && + otherUserData.id == id && + otherUserData.photoUrl == photoUrl && + otherUserData.idToken == idToken; + } +} + +class GoogleSignInTokenData { + GoogleSignInTokenData({this.idToken, this.accessToken}); + String idToken; + String accessToken; + + @override + int get hashCode => hash2(idToken, accessToken); + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (other is! GoogleSignInTokenData) return false; + final GoogleSignInTokenData otherTokenData = other; + return otherTokenData.idToken == idToken && + otherTokenData.accessToken == accessToken; + } +} diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart new file mode 100644 index 000000000000..eb60f00cba63 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart @@ -0,0 +1,29 @@ +// 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 '../google_sign_in_platform_interface.dart'; + +/// Converts user data coming from native code into the proper platform interface type. +GoogleSignInUserData getUserDataFromMap(Map data) { + if (data == null) { + return null; + } + return GoogleSignInUserData( + displayName: data['displayName'], + email: data['email'], + id: data['id'], + photoUrl: data['photoUrl'], + idToken: data['idToken']); +} + +/// Converts token data coming from native code into the proper platform interface type. +GoogleSignInTokenData getTokenDataFromMap(Map data) { + if (data == null) { + return null; + } + return GoogleSignInTokenData( + idToken: data['idToken'], + accessToken: data['accessToken'], + ); +} diff --git a/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml b/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..c791b3b7daa1 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml @@ -0,0 +1,22 @@ +name: google_sign_in_platform_interface +description: A common platform interface for the google_sign_in plugin. +author: Flutter Team +homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_platform_interface +# NOTE: We strongly prefer non-breaking changes, even at the expense of a +# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes +version: 1.0.1 + +dependencies: + flutter: + sdk: flutter + meta: ^1.0.5 + quiver: ">=2.0.0 <3.0.0" + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^4.1.1 + +environment: + sdk: ">=2.0.0-dev.28.0 <3.0.0" + flutter: ">=1.5.0 <2.0.0" diff --git a/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart b/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart new file mode 100644 index 000000000000..f411b8992821 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart @@ -0,0 +1,37 @@ +// 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:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +void main() { + group('$GoogleSignInPlatform', () { + test('$MethodChannelGoogleSignIn is the default instance', () { + expect(GoogleSignInPlatform.instance, isA()); + }); + + test('Cannot be implemented with `implements`', () { + expect(() { + GoogleSignInPlatform.instance = ImplementsGoogleSignInPlatform(); + }, throwsAssertionError); + }); + + test('Can be extended', () { + GoogleSignInPlatform.instance = ExtendsGoogleSignInPlatform(); + }); + + test('Can be mocked with `implements`', () { + final ImplementsGoogleSignInPlatform mock = + ImplementsGoogleSignInPlatform(); + when(mock.isMock).thenReturn(true); + GoogleSignInPlatform.instance = mock; + }); + }); +} + +class ImplementsGoogleSignInPlatform extends Mock + implements GoogleSignInPlatform {} + +class ExtendsGoogleSignInPlatform extends GoogleSignInPlatform {} diff --git a/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart b/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart new file mode 100644 index 000000000000..13de6acf0748 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart @@ -0,0 +1,127 @@ +// 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:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:google_sign_in_platform_interface/src/types.dart'; +import 'package:google_sign_in_platform_interface/src/utils.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const Map kUserData = { + "email": "john.doe@gmail.com", + "id": "8162538176523816253123", + "photoUrl": "https://lh5.googleusercontent.com/photo.jpg", + "displayName": "John Doe", +}; + +const Map kTokenData = { + 'idToken': '123', + 'accessToken': '456', +}; + +const Map kDefaultResponses = { + 'init': null, + 'signInSilently': kUserData, + 'signIn': kUserData, + 'signOut': null, + 'disconnect': null, + 'isSignedIn': true, + 'getTokens': kTokenData, +}; + +final GoogleSignInUserData kUser = getUserDataFromMap(kUserData); +final GoogleSignInTokenData kToken = getTokenDataFromMap(kTokenData); + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$MethodChannelGoogleSignIn', () { + final MethodChannelGoogleSignIn googleSignIn = MethodChannelGoogleSignIn(); + final MethodChannel channel = googleSignIn.channel; + + final List log = []; + Map responses; // Some tests mutate some kDefaultResponses + + setUp(() { + responses = Map.from(kDefaultResponses); + channel.setMockMethodCallHandler((MethodCall methodCall) { + log.add(methodCall); + final dynamic response = responses[methodCall.method]; + if (response != null && response is Exception) { + return Future.error('$response'); + } + return Future.value(response); + }); + log.clear(); + }); + + test('signInSilently transforms platform data to GoogleSignInUserData', + () async { + final dynamic response = await googleSignIn.signInSilently(); + expect(response, kUser); + }); + test('signInSilently Exceptions -> throws', () async { + responses['signInSilently'] = Exception('Not a user'); + expect(googleSignIn.signInSilently(), + throwsA(isInstanceOf())); + }); + + test('signIn transforms platform data to GoogleSignInUserData', () async { + final dynamic response = await googleSignIn.signIn(); + expect(response, kUser); + }); + test('signIn Exceptions -> throws', () async { + responses['signIn'] = Exception('Not a user'); + expect(googleSignIn.signIn(), throwsA(isInstanceOf())); + }); + + test('getTokens transforms platform data to GoogleSignInTokenData', + () async { + final dynamic response = await googleSignIn.getTokens( + email: 'example@example.com', shouldRecoverAuth: false); + expect(response, kToken); + expect( + log[0], + isMethodCall('getTokens', arguments: { + 'email': 'example@example.com', + 'shouldRecoverAuth': false, + })); + }); + + test('Other functions pass through arguments to the channel', () async { + final Map tests = { + () { + googleSignIn.init( + hostedDomain: 'example.com', + scopes: ['two', 'scopes'], + signInOption: SignInOption.games, + clientId: 'UNUSED!'); + }: isMethodCall('init', arguments: { + 'hostedDomain': 'example.com', + 'scopes': ['two', 'scopes'], + 'signInOption': 'SignInOption.games', + }), + () { + googleSignIn.getTokens( + email: 'example@example.com', shouldRecoverAuth: false); + }: isMethodCall('getTokens', arguments: { + 'email': 'example@example.com', + 'shouldRecoverAuth': false, + }), + () { + googleSignIn.clearAuthCache(token: 'abc'); + }: isMethodCall('clearAuthCache', arguments: { + 'token': 'abc', + }), + googleSignIn.signOut: isMethodCall('signOut', arguments: null), + googleSignIn.disconnect: isMethodCall('disconnect', arguments: null), + googleSignIn.isSignedIn: isMethodCall('isSignedIn', arguments: null), + }; + + tests.keys.forEach((Function f) => f()); + + expect(log, tests.values); + }); + }); +} diff --git a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md new file mode 100644 index 000000000000..b2d801e77cbd --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md @@ -0,0 +1,7 @@ +## 0.8.1 + +* Add podspec to enable compilation on iOS. + +## 0.8.0 + +* Flutter for web initial release \ No newline at end of file diff --git a/packages/google_sign_in/google_sign_in_web/LICENSE b/packages/google_sign_in/google_sign_in_web/LICENSE new file mode 100644 index 000000000000..4da9688730d1 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/LICENSE @@ -0,0 +1,26 @@ +Copyright 2016, the Flutter project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/google_sign_in/google_sign_in_web/README.md b/packages/google_sign_in/google_sign_in_web/README.md new file mode 100644 index 000000000000..182b21018aa4 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/README.md @@ -0,0 +1,80 @@ +# google_sign_in_web + +The web implementation of [google_sign_in](https://pub.dev/google_sign_in/google_sign_in) + +## Usage + +### Import the package +To use this plugin, follow the [plugin installation instructions](https://pub.dartlang.org/packages/google_sign_in#pub-pkg-tab-installing). + +Remember that for web plugins you need to depend both on the "native" version that provides the Dart interface that you'll use in your app), and the "web" version, that provides the implementation of the plugin for the web platform. + +This is what the above means to your `pubspec.yaml`: + +``` +... +dependencies: + ... + google_sign_in: ^4.0.14 + google_sign_in_web: ^0.8.0 + ... +... +``` + +### Web integration + +First, go through the instructions [here](https://developers.google.com/identity/sign-in/web/sign-in#before_you_begin) to create your Google Sign-In OAuth client ID. + +On your `web/index.html` file, add the following `meta` tag, somewhere in the +`head` of the document: + +``` + +``` + +Read the rest of the instructions if you need to add extra APIs (like Google People API). + + +### Use the plugin +Add the following import to your Dart code: + +```dart +import 'package:google_sign_in/google_sign_in.dart'; +``` + +Initialize GoogleSignIn with the scopes you want: + +```dart +GoogleSignIn _googleSignIn = GoogleSignIn( + scopes: [ + 'email', + 'https://www.googleapis.com/auth/contacts.readonly', + ], +); +``` +[Full list of available scopes](https://developers.google.com/identity/protocols/googlescopes). + +You can now use the `GoogleSignIn` class to authenticate in your Dart code, e.g. + +```dart +Future _handleSignIn() async { + try { + await _googleSignIn.signIn(); + } catch (error) { + print(error); + } +} +``` + +## Example + +Find the example wiring in the [Google sign-in example application](https://github.com/flutter/plugins/blob/master/packages/google_sign_in/google_sign_in/example/lib/main.dart). + +## API details + +See the [google_sign_in.dart](https://github.com/flutter/plugins/blob/master/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart) for more API details. + +## Issues and feedback + +Please file [issues](https://github.com/flutter/flutter/issues/new) +to send feedback or report a bug. Thank you! diff --git a/packages/google_sign_in/google_sign_in_web/analysis_options.yaml b/packages/google_sign_in/google_sign_in_web/analysis_options.yaml new file mode 100644 index 000000000000..af557b7ac2e0 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/analysis_options.yaml @@ -0,0 +1,12 @@ +# This is a temporary file to allow us to land a new set of linter rules in a +# series of manageable patches instead of one gigantic PR. It disables some of +# the new lints that are already failing on this plugin, for this plugin. It +# should be deleted and the failing lints addressed as soon as possible. + +include: ../../../analysis_options.yaml + +analyzer: + errors: + public_member_api_docs: ignore + unused_element: ignore + unawaited_futures: ignore diff --git a/packages/google_sign_in/google_sign_in_web/ios/google_sign_in_web.podspec b/packages/google_sign_in/google_sign_in_web/ios/google_sign_in_web.podspec new file mode 100644 index 000000000000..5e192172eb4b --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/ios/google_sign_in_web.podspec @@ -0,0 +1,21 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'google_sign_in_web' + s.version = '0.8.1' + s.summary = 'No-op implementation of google_sign_in_web web plugin to avoid build issues on iOS' + s.description = <<-DESC + temp fake google_sign_in_web plugin + DESC + s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_web' + s.license = { :file => '../LICENSE' } + s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + + s.ios.deployment_target = '8.0' + end + \ No newline at end of file diff --git a/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart new file mode 100644 index 000000000000..c7889c9107e4 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart @@ -0,0 +1,151 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html' as html; + +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:js/js.dart'; +import 'package:meta/meta.dart'; + +import 'src/generated/gapiauth2.dart' as auth2; +// TODO: Remove once this lands https://github.com/dart-lang/language/issues/671 +import 'src/generated/gapiauth2.dart' show GoogleAuthExtensions; +import 'src/load_gapi.dart' as gapi; +import 'src/utils.dart' show gapiUserToPluginUserData; + +const String _kClientIdMetaSelector = 'meta[name=google-signin-client_id]'; +const String _kClientIdAttributeName = 'content'; + +@visibleForTesting +String gapiUrl = 'https://apis.google.com/js/platform.js'; + +/// Implementation of the google_sign_in plugin for Web +class GoogleSignInPlugin extends GoogleSignInPlatform { + GoogleSignInPlugin() { + _autoDetectedClientId = html + .querySelector(_kClientIdMetaSelector) + ?.getAttribute(_kClientIdAttributeName); + + _isGapiInitialized = gapi.inject(gapiUrl).then((_) => gapi.init()); + } + + Future _isGapiInitialized; + + @visibleForTesting + Future get initialized => _isGapiInitialized; + + String _autoDetectedClientId; + FutureOr _lastSeenUser; + + static void registerWith(Registrar registrar) { + GoogleSignInPlatform.instance = GoogleSignInPlugin(); + } + + @override + Future init( + {@required String hostedDomain, + List scopes = const [], + SignInOption signInOption = SignInOption.standard, + String clientId}) async { + final String appClientId = clientId ?? _autoDetectedClientId; + assert( + appClientId != null, + 'ClientID not set. Either set it on a ' + ' tag,' + ' or pass clientId when calling init()'); + + assert( + !scopes.any((String scope) => scope.contains(' ')), + 'OAuth 2.0 Scopes for Google APIs can\'t contain spaces.' + 'Check https://developers.google.com/identity/protocols/googlescopes ' + 'for a list of valid OAuth 2.0 scopes.'); + + await initialized; + + final auth2.GoogleAuth auth = auth2.init(auth2.ClientConfig( + hosted_domain: hostedDomain, + // The js lib wants a space-separated list of values + scope: scopes.join(' '), + client_id: appClientId, + )); + + // Subscribe to changes in the auth instance returned by init, + // and cache the _lastSeenUser as we get notified of new values. + final Completer initUserCompleter = + Completer(); + + auth.currentUser.listen(allowInterop((auth2.GoogleUser nextUser) { + if (!initUserCompleter.isCompleted) { + initUserCompleter.complete(nextUser); + } else { + _lastSeenUser = nextUser; + } + })); + _lastSeenUser = initUserCompleter.future; + + return null; + } + + @override + Future signInSilently() async { + await initialized; + + return gapiUserToPluginUserData(await _lastSeenUser); + } + + @override + Future signIn() async { + await initialized; + + return gapiUserToPluginUserData(await auth2.getAuthInstance().signIn()); + } + + @override + Future getTokens( + {@required String email, bool shouldRecoverAuth}) async { + await initialized; + + final auth2.GoogleUser currentUser = + auth2.getAuthInstance()?.currentUser?.get(); + final auth2.AuthResponse response = currentUser.getAuthResponse(); + + return GoogleSignInTokenData( + idToken: response.id_token, accessToken: response.access_token); + } + + @override + Future signOut() async { + await initialized; + + return auth2.getAuthInstance().signOut(); + } + + @override + Future disconnect() async { + await initialized; + + final auth2.GoogleUser currentUser = + auth2.getAuthInstance()?.currentUser?.get(); + return currentUser.disconnect(); + } + + @override + Future isSignedIn() async { + await initialized; + + final auth2.GoogleUser currentUser = + auth2.getAuthInstance()?.currentUser?.get(); + return currentUser.isSignedIn(); + } + + @override + Future clearAuthCache({String token}) async { + await initialized; + + _lastSeenUser = null; + return auth2.getAuthInstance().disconnect(); + } +} diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart new file mode 100644 index 000000000000..c64d93be223a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart @@ -0,0 +1,459 @@ +// 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. + +@JS() +library gapi; + +import "package:js/js.dart"; +import "package:js/js_util.dart" show promiseToFuture; + +/// Type definitions for Google API Client +/// Project: https://github.com/google/google-api-javascript-client +/// Definitions by: Frank M , grant +/// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +/// TypeScript Version: 2.3 + +/// The OAuth 2.0 token object represents the OAuth 2.0 token and any associated data. +@anonymous +@JS() +abstract class GoogleApiOAuth2TokenObject { + /// The OAuth 2.0 token. Only present in successful responses + external String get access_token; + external set access_token(String v); + + /// Details about the error. Only present in error responses + external String get error; + external set error(String v); + + /// The duration, in seconds, the token is valid for. Only present in successful responses + external String get expires_in; + external set expires_in(String v); + external GoogleApiOAuth2TokenSessionState get session_state; + external set session_state(GoogleApiOAuth2TokenSessionState v); + + /// The Google API scopes related to this token + external String get state; + external set state(String v); + external factory GoogleApiOAuth2TokenObject( + {String access_token, + String error, + String expires_in, + GoogleApiOAuth2TokenSessionState session_state, + String state}); +} + +@anonymous +@JS() +abstract class GoogleApiOAuth2TokenSessionState { + external dynamic /*{ + authuser: string, + }*/ + get extraQueryParams; + external set extraQueryParams( + dynamic + /*{ + authuser: string, + }*/ + v); + external factory GoogleApiOAuth2TokenSessionState( + {dynamic + /*{ + authuser: string, + }*/ + extraQueryParams}); +} + +/// Fix for #8215 +/// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/8215 +/// Usage example: +/// https://developers.google.com/identity/sign-in/web/session-state + +// Module gapi +typedef void LoadCallback( + [dynamic args1, + dynamic args2, + dynamic args3, + dynamic args4, + dynamic args5]); + +@anonymous +@JS() +abstract class LoadConfig { + external LoadCallback get callback; + external set callback(LoadCallback v); + external Function get onerror; + external set onerror(Function v); + external num get timeout; + external set timeout(num v); + external Function get ontimeout; + external set ontimeout(Function v); + external factory LoadConfig( + {LoadCallback callback, + Function onerror, + num timeout, + Function ontimeout}); +} + +/*type CallbackOrConfig = LoadConfig | LoadCallback;*/ +/// Pragmatically initialize gapi class member. +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiloadlibraries-callbackorconfig +@JS("gapi.load") +external void load( + String apiName, dynamic /*LoadConfig|LoadCallback*/ callback); +// End module gapi + +// Module gapi.auth +/// Initiates the OAuth 2.0 authorization process. The browser displays a popup window prompting the user authenticate and authorize. After the user authorizes, the popup closes and the callback function fires. +@JS("gapi.auth.authorize") +external void authorize( + dynamic + /*{ + /** + * The application's client ID. + */ + client_id?: string; + /** + * If true, then login uses "immediate mode", which means that the token is refreshed behind the scenes, and no UI is shown to the user. + */ + immediate?: boolean; + /** + * The OAuth 2.0 response type property. Default: token + */ + response_type?: string; + /** + * The auth scope or scopes to authorize. Auth scopes for individual APIs can be found in their documentation. + */ + scope?: any; + /** + * The user to sign in as. -1 to toggle a multi-account chooser, 0 to default to the user's current account, and 1 to automatically sign in if the user is signed into Google Plus. + */ + authuser?: number; + }*/ + params, + dynamic callback(GoogleApiOAuth2TokenObject token)); + +/// Initializes the authorization feature. Call this when the client loads to prevent popup blockers from blocking the auth window on gapi.auth.authorize calls. +@JS("gapi.auth.init") +external void init(dynamic callback()); + +/// Retrieves the OAuth 2.0 token for the application. +@JS("gapi.auth.getToken") +external GoogleApiOAuth2TokenObject getToken(); + +/// Sets the OAuth 2.0 token for the application. +@JS("gapi.auth.setToken") +external void setToken(GoogleApiOAuth2TokenObject token); + +/// Initiates the client-side Google+ Sign-In OAuth 2.0 flow. +/// When the method is called, the OAuth 2.0 authorization dialog is displayed to the user and when they accept, the callback function is called. +@JS("gapi.auth.signIn") +external void signIn( + dynamic + /*{ + /** + * Your OAuth 2.0 client ID that you obtained from the Google Developers Console. + */ + clientid?: string; + /** + * Directs the sign-in button to store user and session information in a session cookie and HTML5 session storage on the user's client for the purpose of minimizing HTTP traffic and distinguishing between multiple Google accounts a user might be signed into. + */ + cookiepolicy?: string; + /** + * A function in the global namespace, which is called when the sign-in button is rendered and also called after a sign-in flow completes. + */ + callback?: () => void; + /** + * If true, all previously granted scopes remain granted in each incremental request, for incremental authorization. The default value true is correct for most use cases; use false only if employing delegated auth, where you pass the bearer token to a less-trusted component with lower programmatic authority. + */ + includegrantedscopes?: boolean; + /** + * If your app will write moments, list the full URI of the types of moments that you intend to write. + */ + requestvisibleactions?: any; + /** + * The OAuth 2.0 scopes for the APIs that you would like to use as a space-delimited list. + */ + scope?: any; + /** + * If you have an Android app, you can drive automatic Android downloads from your web sign-in flow. + */ + apppackagename?: string; + }*/ + params); + +/// Signs a user out of your app without logging the user out of Google. This method will only work when the user is signed in with Google+ Sign-In. +@JS("gapi.auth.signOut") +external void signOut(); +// End module gapi.auth + +// Module gapi.client +@anonymous +@JS() +abstract class RequestOptions { + /// The URL to handle the request + external String get path; + external set path(String v); + + /// The HTTP request method to use. Default is GET + external String get method; + external set method(String v); + + /// URL params in key-value pair form + external dynamic get params; + external set params(dynamic v); + + /// Additional HTTP request headers + external dynamic get headers; + external set headers(dynamic v); + + /// The HTTP request body (applies to PUT or POST). + external dynamic get body; + external set body(dynamic v); + + /// If supplied, the request is executed immediately and no gapi.client.HttpRequest object is returned + external dynamic Function() get callback; + external set callback(dynamic Function() v); + external factory RequestOptions( + {String path, + String method, + dynamic params, + dynamic headers, + dynamic body, + dynamic Function() callback}); +} + +@anonymous +@JS() +abstract class _RequestOptions { + @JS("gapi.client.init") + external Promise client_init( + dynamic + /*{ + /** + * The API Key to use. + */ + apiKey?: string; + /** + * An array of discovery doc URLs or discovery doc JSON objects. + */ + discoveryDocs?: string[]; + /** + * The app's client ID, found and created in the Google Developers Console. + */ + clientId?: string; + /** + * The scopes to request, as a space-delimited string. + */ + scope?: string, + + hosted_domain?: string; + }*/ + args); +} + +extension RequestOptionsExtensions on RequestOptions {} + +@anonymous +@JS() +abstract class TokenObject { + /// The access token to use in requests. + external String get access_token; + external set access_token(String v); + external factory TokenObject({String access_token}); +} + +/// Creates a HTTP request for making RESTful requests. +/// An object encapsulating the various arguments for this method. +@JS("gapi.client.request") +external HttpRequest request(RequestOptions args); + +/// Creates an RPC Request directly. The method name and version identify the method to be executed and the RPC params are provided upon RPC creation. +@JS("gapi.client.rpcRequest") +external RpcRequest rpcRequest(String method, + [String version, dynamic rpcParams]); + +/// Sets the API key for the application. +@JS("gapi.client.setApiKey") +external void setApiKey(String apiKey); + +/// Retrieves the OAuth 2.0 token for the application. +@JS("gapi.client.getToken") +external GoogleApiOAuth2TokenObject client_getToken(); + +/// Sets the authentication token to use in requests. +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiclientsettokentokenobject +@JS("gapi.client.setToken") +external void client_setToken(TokenObject /*TokenObject|Null*/ token); + +@anonymous +@JS() +abstract class HttpRequestFulfilled { + external T get result; + external set result(T v); + external String get body; + external set body(String v); + external List get headers; + external set headers(List v); + external num get status; + external set status(num v); + external String get statusText; + external set statusText(String v); + external factory HttpRequestFulfilled( + {T result, + String body, + List headers, + num status, + String statusText}); +} + +@anonymous +@JS() +abstract class _HttpRequestFulfilled { + /*external Promise client_load(String name, String version);*/ + /*external void client_load(String name, String version, dynamic callback(), + [String url]); +*/ + @JS("gapi.client.load") + external dynamic /*Promise|void*/ client_load( + String name, String version, + [dynamic callback(), String url]); +} + +extension HttpRequestFulfilledExtensions on HttpRequestFulfilled {} + +@anonymous +@JS() +abstract class HttpRequestRejected { + external dynamic /*dynamic|bool*/ get result; + external set result(dynamic /*dynamic|bool*/ v); + external String get body; + external set body(String v); + external List get headers; + external set headers(List v); + external num get status; + external set status(num v); + external String get statusText; + external set statusText(String v); + external factory HttpRequestRejected( + {dynamic /*dynamic|bool*/ result, + String body, + List headers, + num status, + String statusText}); +} + +/// HttpRequest supports promises. +/// See Google API Client JavaScript Using Promises https://developers.google.com/api-client-library/javascript/features/promises +@JS("gapi.client.HttpRequestPromise") +class HttpRequestPromise { + // @Ignore + HttpRequestPromise.fakeConstructor$(); +} + +@JS("gapi.client.HttpRequestPromise") +abstract class _HttpRequestPromise { + /// Taken and adapted from https://github.com/Microsoft/TypeScript/blob/v2.3.1/lib/lib.es5.d.ts#L1343 + external Promise then/**/( + [dynamic /*TResult1|PromiseLike Function(HttpRequestFulfilled)|dynamic|Null*/ onfulfilled, + dynamic /*TResult2|PromiseLike Function(HttpRequestRejected)|dynamic|Null*/ onrejected, + dynamic opt_context]); +} + +extension HttpRequestPromiseExtensions on HttpRequestPromise { + Future then( + [dynamic /*TResult1|PromiseLike Function(HttpRequestFulfilled)|dynamic|Null*/ onfulfilled, + dynamic /*TResult2|PromiseLike Function(HttpRequestRejected)|dynamic|Null*/ onrejected, + dynamic opt_context]) { + final Object t = this; + final _HttpRequestPromise tt = t; + return promiseToFuture(tt.then(onfulfilled, onrejected, opt_context)); + } +} + +/// An object encapsulating an HTTP request. This object is not instantiated directly, rather it is returned by gapi.client.request. +@JS("gapi.client.HttpRequest") +class HttpRequest extends HttpRequestPromise { + // @Ignore + HttpRequest.fakeConstructor$() : super.fakeConstructor$(); + + /// Executes the request and runs the supplied callback on response. + external void execute( + dynamic callback( + + /// contains the response parsed as JSON. If the response is not JSON, this field will be false. + T jsonResp, + + /// is the HTTP response. It is JSON, and can be parsed to an object + dynamic + /*{ + body: string; + headers: any[]; + status: number; + statusText: string; + }*/ + rawResp)); +} + +/// Represents an HTTP Batch operation. Individual HTTP requests are added with the add method and the batch is executed using execute. +@JS("gapi.client.HttpBatch") +class HttpBatch { + // @Ignore + HttpBatch.fakeConstructor$(); + + /// Adds a gapi.client.HttpRequest to the batch. + external void add(HttpRequest httpRequest, + [dynamic + /*{ + /** + * Identifies the response for this request in the map of batch responses. If one is not provided, the system generates a random ID. + */ + id: string; + callback: ( + /** + * is the response for this request only. Its format is defined by the API method being called. + */ + individualResponse: any, + /** + * is the raw batch ID-response map as a string. It contains all responses to all requests in the batch. + */ + rawBatchResponse: any + ) => any + }*/ + opt_params]); + + /// Executes all requests in the batch. The supplied callback is executed on success or failure. + external void execute( + dynamic callback( + + /// is an ID-response map of each requests response. + dynamic responseMap, + + /// is the same response, but as an unparsed JSON-string. + String rawBatchResponse)); +} + +/// Similar to gapi.client.HttpRequest except this object encapsulates requests generated by registered methods. +@JS("gapi.client.RpcRequest") +class RpcRequest { + // @Ignore + RpcRequest.fakeConstructor$(); + + /// Executes the request and runs the supplied callback with the response. + external void callback( + void callback( + + /// contains the response parsed as JSON. If the response is not JSON, this field will be false. + dynamic jsonResp, + + /// is the same as jsonResp, except it is a raw string that has not been parsed. It is typically used when the response is not JSON. + String rawResp)); +} + +// End module gapi.client +@JS() +abstract class Promise { + external factory Promise( + void executor(void resolve(T result), Function reject)); + external Promise then(void onFulfilled(T result), [Function onRejected]); +} diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart new file mode 100644 index 000000000000..5071f93e6e30 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart @@ -0,0 +1,464 @@ +// 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. + +@JS() +library gapiauth2; + +import "package:js/js.dart"; +import "package:js/js_util.dart" show promiseToFuture; + +/// Type definitions for non-npm package Google Sign-In API 0.0 +/// Project: https://developers.google.com/identity/sign-in/web/ +/// Definitions by: Derek Lawless +/// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +/// TypeScript Version: 2.3 + +/// + +// Module gapi.auth2 +/// GoogleAuth is a singleton class that provides methods to allow the user to sign in with a Google account, +/// get the user's current sign-in status, get specific data from the user's Google profile, +/// request additional scopes, and sign out from the current account. +@JS("gapi.auth2.GoogleAuth") +class GoogleAuth { + // @Ignore + GoogleAuth.fakeConstructor$(); + external IsSignedIn get isSignedIn; + external set isSignedIn(IsSignedIn v); + external CurrentUser get currentUser; + external set currentUser(CurrentUser v); + + /// Calls the onInit function when the GoogleAuth object is fully initialized, or calls the onFailure function if + /// initialization fails. + external dynamic then(dynamic onInit(GoogleAuth googleAuth), + [dynamic onFailure(dynamic /*{error: string, details: string}*/ reason)]); + + /// Signs out all accounts from the application. + external dynamic signOut(); + + /// Revokes all of the scopes that the user granted. + external dynamic disconnect(); + + /// Attaches the sign-in flow to the specified container's click handler. + external dynamic attachClickHandler( + dynamic container, + SigninOptions options, + dynamic onsuccess(GoogleUser googleUser), + dynamic onfailure(String reason)); +} + +@anonymous +@JS() +abstract class _GoogleAuth { + external Promise signIn( + [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]); + external Promise grantOfflineAccess( + [OfflineAccessOptions options]); +} + +extension GoogleAuthExtensions on GoogleAuth { + Future signIn( + [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]) { + final Object t = this; + final _GoogleAuth tt = t; + return promiseToFuture(tt.signIn(options)); + } + + Future grantOfflineAccess( + [OfflineAccessOptions options]) { + final Object t = this; + final _GoogleAuth tt = t; + return promiseToFuture(tt.grantOfflineAccess(options)); + } +} + +@anonymous +@JS() +abstract class IsSignedIn { + /// Returns whether the current user is currently signed in. + external bool get(); + + /// Listen for changes in the current user's sign-in state. + external void listen(dynamic listener(bool signedIn)); +} + +@anonymous +@JS() +abstract class CurrentUser { + /// Returns a GoogleUser object that represents the current user. Note that in a newly-initialized + /// GoogleAuth instance, the current user has not been set. Use the currentUser.listen() method or the + /// GoogleAuth.then() to get an initialized GoogleAuth instance. + external GoogleUser get(); + + /// Listen for changes in currentUser. + external void listen(dynamic listener(GoogleUser user)); +} + +@anonymous +@JS() +abstract class SigninOptions { + /// The package name of the Android app to install over the air. + /// See Android app installs from your web site: + /// https://developers.google.com/identity/sign-in/web/android-app-installs + external String get app_package_name; + external set app_package_name(String v); + + /// Fetch users' basic profile information when they sign in. + /// Adds 'profile', 'email' and 'openid' to the requested scopes. + /// True if unspecified. + external bool get fetch_basic_profile; + external set fetch_basic_profile(bool v); + + /// Specifies whether to prompt the user for re-authentication. + /// See OpenID Connect Request Parameters: + /// https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters + external String get prompt; + external set prompt(String v); + + /// The scopes to request, as a space-delimited string. + /// Optional if fetch_basic_profile is not set to false. + external String get scope; + external set scope(String v); + + /// The UX mode to use for the sign-in flow. + /// By default, it will open the consent flow in a popup. + external String /*'popup'|'redirect'*/ get ux_mode; + external set ux_mode(String /*'popup'|'redirect'*/ v); + + /// If using ux_mode='redirect', this parameter allows you to override the default redirect_uri that will be used at the end of the consent flow. + /// The default redirect_uri is the current URL stripped of query parameters and hash fragment. + external String get redirect_uri; + external set redirect_uri(String v); + external factory SigninOptions( + {String app_package_name, + bool fetch_basic_profile, + String prompt, + String scope, + String /*'popup'|'redirect'*/ ux_mode, + String redirect_uri}); +} + +/// Definitions by: John +/// Interface that represents the different configuration parameters for the GoogleAuth.grantOfflineAccess(options) method. +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2offlineaccessoptions +@anonymous +@JS() +abstract class OfflineAccessOptions { + external String get scope; + external set scope(String v); + external String /*'select_account'|'consent'*/ get prompt; + external set prompt(String /*'select_account'|'consent'*/ v); + external String get app_package_name; + external set app_package_name(String v); + external factory OfflineAccessOptions( + {String scope, + String /*'select_account'|'consent'*/ prompt, + String app_package_name}); +} + +/// Interface that represents the different configuration parameters for the gapi.auth2.init method. +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2clientconfig +@anonymous +@JS() +abstract class ClientConfig { + /// The app's client ID, found and created in the Google Developers Console. + external String get client_id; + external set client_id(String v); + + /// The domains for which to create sign-in cookies. Either a URI, single_host_origin, or none. + /// Defaults to single_host_origin if unspecified. + external String get cookie_policy; + external set cookie_policy(String v); + + /// The scopes to request, as a space-delimited string. Optional if fetch_basic_profile is not set to false. + external String get scope; + external set scope(String v); + + /// Fetch users' basic profile information when they sign in. Adds 'profile' and 'email' to the requested scopes. True if unspecified. + external bool get fetch_basic_profile; + external set fetch_basic_profile(bool v); + + /// The Google Apps domain to which users must belong to sign in. This is susceptible to modification by clients, + /// so be sure to verify the hosted domain property of the returned user. Use GoogleUser.getHostedDomain() on the client, + /// and the hd claim in the ID Token on the server to verify the domain is what you expected. + external String get hosted_domain; + external set hosted_domain(String v); + + /// Used only for OpenID 2.0 client migration. Set to the value of the realm that you are currently using for OpenID 2.0, + /// as described in OpenID 2.0 (Migration). + external String get openid_realm; + external set openid_realm(String v); + + /// The UX mode to use for the sign-in flow. + /// By default, it will open the consent flow in a popup. + external String /*'popup'|'redirect'*/ get ux_mode; + external set ux_mode(String /*'popup'|'redirect'*/ v); + + /// If using ux_mode='redirect', this parameter allows you to override the default redirect_uri that will be used at the end of the consent flow. + /// The default redirect_uri is the current URL stripped of query parameters and hash fragment. + external String get redirect_uri; + external set redirect_uri(String v); + external factory ClientConfig( + {String client_id, + String cookie_policy, + String scope, + bool fetch_basic_profile, + String hosted_domain, + String openid_realm, + String /*'popup'|'redirect'*/ ux_mode, + String redirect_uri}); +} + +@JS("gapi.auth2.SigninOptionsBuilder") +class SigninOptionsBuilder { + // @Ignore + SigninOptionsBuilder.fakeConstructor$(); + external dynamic setAppPackageName(String name); + external dynamic setFetchBasicProfile(bool fetch); + external dynamic setPrompt(String prompt); + external dynamic setScope(String scope); +} + +@anonymous +@JS() +abstract class BasicProfile { + external String getId(); + external String getName(); + external String getGivenName(); + external String getFamilyName(); + external String getImageUrl(); + external String getEmail(); +} + +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authresponse +@anonymous +@JS() +abstract class AuthResponse { + external String get access_token; + external set access_token(String v); + external String get id_token; + external set id_token(String v); + external String get login_hint; + external set login_hint(String v); + external String get scope; + external set scope(String v); + external num get expires_in; + external set expires_in(num v); + external num get first_issued_at; + external set first_issued_at(num v); + external num get expires_at; + external set expires_at(num v); + external factory AuthResponse( + {String access_token, + String id_token, + String login_hint, + String scope, + num expires_in, + num first_issued_at, + num expires_at}); +} + +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeconfig +@anonymous +@JS() +abstract class AuthorizeConfig { + external String get client_id; + external set client_id(String v); + external String get scope; + external set scope(String v); + external String get response_type; + external set response_type(String v); + external String get prompt; + external set prompt(String v); + external String get cookie_policy; + external set cookie_policy(String v); + external String get hosted_domain; + external set hosted_domain(String v); + external String get login_hint; + external set login_hint(String v); + external String get app_package_name; + external set app_package_name(String v); + external String get openid_realm; + external set openid_realm(String v); + external bool get include_granted_scopes; + external set include_granted_scopes(bool v); + external factory AuthorizeConfig( + {String client_id, + String scope, + String response_type, + String prompt, + String cookie_policy, + String hosted_domain, + String login_hint, + String app_package_name, + String openid_realm, + bool include_granted_scopes}); +} + +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeresponse +@anonymous +@JS() +abstract class AuthorizeResponse { + external String get access_token; + external set access_token(String v); + external String get id_token; + external set id_token(String v); + external String get code; + external set code(String v); + external String get scope; + external set scope(String v); + external num get expires_in; + external set expires_in(num v); + external num get first_issued_at; + external set first_issued_at(num v); + external num get expires_at; + external set expires_at(num v); + external String get error; + external set error(String v); + external String get error_subtype; + external set error_subtype(String v); + external factory AuthorizeResponse( + {String access_token, + String id_token, + String code, + String scope, + num expires_in, + num first_issued_at, + num expires_at, + String error, + String error_subtype}); +} + +/// A GoogleUser object represents one user account. +@anonymous +@JS() +abstract class GoogleUser { + /// Get the user's unique ID string. + external String getId(); + + /// Returns true if the user is signed in. + external bool isSignedIn(); + + /// Get the user's Google Apps domain if the user signed in with a Google Apps account. + external String getHostedDomain(); + + /// Get the scopes that the user granted as a space-delimited string. + external String getGrantedScopes(); + + /// Get the user's basic profile information. + external BasicProfile getBasicProfile(); + + /// Get the response object from the user's auth session. + external AuthResponse getAuthResponse([bool includeAuthorizationData]); + + /// Returns true if the user granted the specified scopes. + external bool hasGrantedScopes(String scopes); + + /// Signs in the user. Use this method to request additional scopes for incremental + /// authorization or to sign in a user after the user has signed out. + /// When you use GoogleUser.signIn(), the sign-in flow skips the account chooser step. + /// See GoogleAuth.signIn(). + external dynamic signIn( + [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]); + + /// See GoogleUser.signIn() + external dynamic grant( + [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]); + + /// Get permission from the user to access the specified scopes offline. + /// When you use GoogleUser.grantOfflineAccess(), the sign-in flow skips the account chooser step. + /// See GoogleUser.grantOfflineAccess(). + external void grantOfflineAccess(String scopes); + + /// Revokes all of the scopes that the user granted. + external void disconnect(); +} + +@anonymous +@JS() +abstract class _GoogleUser { + external Promise reloadAuthResponse(); +} + +extension GoogleUserExtensions on GoogleUser { + Future reloadAuthResponse() { + final Object t = this; + final _GoogleUser tt = t; + return promiseToFuture(tt.reloadAuthResponse()); + } +} + +/// Initializes the GoogleAuth object. +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2initparams +@JS("gapi.auth2.init") +external GoogleAuth init(ClientConfig params); + +/// Returns the GoogleAuth object. You must initialize the GoogleAuth object with gapi.auth2.init() before calling this method. +@JS("gapi.auth2.getAuthInstance") +external GoogleAuth getAuthInstance(); + +/// Performs a one time OAuth 2.0 authorization. +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeparams-callback +@JS("gapi.auth2.authorize") +external void authorize( + AuthorizeConfig params, void callback(AuthorizeResponse response)); +// End module gapi.auth2 + +// Module gapi.signin2 +@JS("gapi.signin2.render") +external void render( + dynamic id, + dynamic + /*{ + /** + * The auth scope or scopes to authorize. Auth scopes for individual APIs can be found in their documentation. + */ + scope?: string; + + /** + * The width of the button in pixels (default: 120). + */ + width?: number; + + /** + * The height of the button in pixels (default: 36). + */ + height?: number; + + /** + * Display long labels such as "Sign in with Google" rather than "Sign in" (default: false). + */ + longtitle?: boolean; + + /** + * The color theme of the button: either light or dark (default: light). + */ + theme?: string; + + /** + * The callback function to call when a user successfully signs in (default: none). + */ + onsuccess?(user: auth2.GoogleUser): void; + + /** + * The callback function to call when sign-in fails (default: none). + */ + onfailure?(reason: { error: string }): void; + + /** + * The package name of the Android app to install over the air. See + * Android app installs from your web site. + * Optional. (default: none) + */ + app_package_name?: string; + }*/ + options); + +// End module gapi.signin2 +@JS() +abstract class Promise { + external factory Promise( + void executor(void resolve(T result), Function reject)); + external Promise then(void onFulfilled(T result), [Function onRejected]); +} diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart b/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart new file mode 100644 index 000000000000..8273f39630f9 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart @@ -0,0 +1,55 @@ +// 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. + +@JS() +library gapi_onload; + +import 'dart:async'; + +import 'package:js/js.dart'; +import 'package:meta/meta.dart'; + +import 'generated/gapi.dart' as gapi; +import 'utils.dart' show injectJSLibraries; + +@JS() +external set gapiOnloadCallback(Function callback); + +// This name must match the external setter above +@visibleForTesting +const String kGapiOnloadCallbackFunctionName = "gapiOnloadCallback"; +String _addOnloadToScript(String url) => url.startsWith('data:') + ? url + : '$url?onload=$kGapiOnloadCallbackFunctionName'; + +/// Injects the GAPI library by its [url], and other additional [libraries]. +/// +/// GAPI has an onload API where it'll call a callback when it's ready, JSONP style. +Future inject(String url, {List libraries = const []}) { + // Inject the GAPI library, and configure the onload global + final Completer gapiOnLoad = Completer(); + gapiOnloadCallback = allowInterop(() { + // Funnel the GAPI onload to a Dart future + gapiOnLoad.complete(); + }); + + // Attach the onload callback to the main url + final List allLibraries = [_addOnloadToScript(url)] + ..addAll(libraries); + + return Future.wait( + >[injectJSLibraries(allLibraries), gapiOnLoad.future]); +} + +/// Initialize the global gapi object so 'auth2' can be used. +/// Returns a promise that resolves when 'auth2' is ready. +Future init() { + final Completer gapiLoadCompleter = Completer(); + gapi.load('auth2', allowInterop(() { + gapiLoadCompleter.complete(); + })); + + // After this resolves, we can use gapi.auth2! + return gapiLoadCompleter.future; +} diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart new file mode 100644 index 000000000000..c8ca8007c1bb --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart @@ -0,0 +1,42 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html' as html; + +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; + +import 'generated/gapiauth2.dart' as auth2; + +/// Injects a bunch of libraries in the and returns a +/// Future that resolves when all load. +Future injectJSLibraries(List libraries, + {html.HtmlElement target /*, Duration timeout */}) { + final List> loading = >[]; + final List tags = []; + + libraries.forEach((String library) { + final html.ScriptElement script = html.ScriptElement() + ..async = true + ..defer = true + ..src = library; + // TODO add a timeout race to fail this future + loading.add(script.onLoad.first); + tags.add(script); + }); + (target ?? html.querySelector('head')).children.addAll(tags); + return Future.wait(loading); +} + +GoogleSignInUserData gapiUserToPluginUserData(auth2.GoogleUser currentUser) { + assert(currentUser != null); + final auth2.BasicProfile profile = currentUser.getBasicProfile(); + return GoogleSignInUserData( + displayName: profile?.getName(), + email: profile?.getEmail(), + id: profile?.getId(), + photoUrl: profile?.getImageUrl(), + idToken: currentUser.getAuthResponse()?.id_token, + ); +} diff --git a/packages/google_sign_in/google_sign_in_web/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/pubspec.yaml new file mode 100644 index 000000000000..ebf20f09f921 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -0,0 +1,31 @@ +name: google_sign_in_web +description: Flutter plugin for Google Sign-In, a secure authentication system + for signing in with a Google account on Android, iOS and Web. +author: Flutter Team +homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_web +version: 0.8.1 + +flutter: + plugin: + platforms: + web: + pluginClass: GoogleSignInPlugin + fileName: google_sign_in_web.dart + +dependencies: + google_sign_in_platform_interface: ^1.0.0 + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + meta: ^1.1.7 + js: ^0.6.1 + +dev_dependencies: + flutter_test: + sdk: flutter + google_sign_in: ^4.0.14 + +environment: + sdk: ">=2.6.0 <3.0.0" + flutter: ">=1.5.0 <2.0.0" diff --git a/packages/google_sign_in/google_sign_in_web/test/auth2_test.dart b/packages/google_sign_in/google_sign_in_web/test/auth2_test.dart new file mode 100644 index 000000000000..b6eb60754b49 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/test/auth2_test.dart @@ -0,0 +1,77 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@TestOn('browser') + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:google_sign_in_web/google_sign_in_web.dart'; +import 'gapi_mocks/gapi_mocks.dart' as gapi_mocks; +import 'utils.dart'; + +void main() { + GoogleSignInTokenData expectedTokenData = + GoogleSignInTokenData(idToken: '70k3n', accessToken: 'access_70k3n'); + + GoogleSignInUserData expectedUserData = GoogleSignInUserData( + displayName: 'Foo Bar', + email: 'foo@example.com', + id: '123', + photoUrl: 'http://example.com/img.jpg', + idToken: expectedTokenData.idToken, + ); + + // The pre-configured use case for the instances of the plugin in this test + gapiUrl = toBase64Url(gapi_mocks.auth2InitSuccess(expectedUserData)); + + GoogleSignInPlugin plugin; + + setUp(() async { + plugin = GoogleSignInPlugin(); + await plugin.initialized; + }); + + test('Init requires clientId', () async { + expect(plugin.init(hostedDomain: ''), throwsAssertionError); + }); + + test('Init doesn\'t accept spaces in scopes', () async { + expect( + plugin.init( + hostedDomain: '', + clientId: '', + scopes: ['scope with spaces'], + ), + throwsAssertionError); + }); + + group('Successful .init, then', () { + setUp(() async { + plugin.init( + hostedDomain: 'foo', + scopes: ['some', 'scope'], + clientId: '1234', + ); + }); + + test('signInSilently', () async { + GoogleSignInUserData actualUser = await plugin.signInSilently(); + + expect(actualUser, expectedUserData); + }); + + test('signIn', () async { + GoogleSignInUserData actualUser = await plugin.signIn(); + + expect(actualUser, expectedUserData); + }); + + test('getTokens', () async { + GoogleSignInTokenData actualToken = + await plugin.getTokens(email: expectedUserData.email); + + expect(actualToken, expectedTokenData); + }); + }); +} diff --git a/packages/google_sign_in/google_sign_in_web/test/gapi_load_test.dart b/packages/google_sign_in/google_sign_in_web/test/gapi_load_test.dart new file mode 100644 index 000000000000..815dcdcfc645 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/test/gapi_load_test.dart @@ -0,0 +1,36 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@TestOn('browser') + +import 'dart:html' as html; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_web/google_sign_in_web.dart'; +import 'gapi_mocks/gapi_mocks.dart' as gapi_mocks; +import 'utils.dart'; + +void main() { + gapiUrl = toBase64Url(gapi_mocks.gapiInitSuccess()); + + test('Plugin is initialized after GAPI fully loads', () async { + expect( + html.querySelector('script[src^="data:"]'), + isNull, + reason: 'Mock script not present before instantiating the plugin', + ); + final GoogleSignInPlugin plugin = GoogleSignInPlugin(); + expect( + html.querySelector('script[src^="data:"]'), + isNotNull, + reason: 'Mock script should be injected', + ); + await plugin.initialized; + expect( + plugin.initialized, + completes, + reason: 'The plugin should complete the future once initialized.', + ); + }); +} diff --git a/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/gapi_mocks.dart b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/gapi_mocks.dart new file mode 100644 index 000000000000..d36cd5edf63a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/gapi_mocks.dart @@ -0,0 +1,14 @@ +// 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. + +library gapi_mocks; + +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; + +import 'src/gapi.dart'; +import 'src/google_user.dart'; +import 'src/test_iife.dart'; + +part 'src/gapi_load.dart'; +part 'src/auth2_init.dart'; diff --git a/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/auth2_init.dart b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/auth2_init.dart new file mode 100644 index 000000000000..5484dc5b61bc --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/auth2_init.dart @@ -0,0 +1,42 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of gapi_mocks; + +// JS mock of a gapi.auth2, with a successfully identified user +String auth2InitSuccess(GoogleSignInUserData userData) => testIife(''' +${gapi()} + +var mockUser = ${googleUser(userData)}; + +function GapiAuth2() {} +GapiAuth2.prototype.init = function (initOptions) { + return { + currentUser: { + listen: (cb) => { + window.setTimeout(() => { + cb(mockUser); + }, 30); + } + } + } +}; + +GapiAuth2.prototype.getAuthInstance = function () { + return { + signIn: () => { + return new Promise((resolve, reject) => { + window.setTimeout(() => { + resolve(mockUser); + }, 30); + }); + }, + currentUser: { + get: () => mockUser, + }, + } +}; + +window.gapi.auth2 = new GapiAuth2(); +'''); diff --git a/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/gapi.dart b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/gapi.dart new file mode 100644 index 000000000000..42d9a8be262c --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/gapi.dart @@ -0,0 +1,12 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// The JS mock of the global gapi object +String gapi() => ''' +function Gapi() {}; +Gapi.prototype.load = function (script, cb) { + window.setTimeout(cb, 30); +}; +window.gapi = new Gapi(); +'''; diff --git a/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/gapi_load.dart b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/gapi_load.dart new file mode 100644 index 000000000000..f390500f6f4e --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/gapi_load.dart @@ -0,0 +1,8 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of gapi_mocks; + +// JS mock of a raw GAPI object. +String gapiInitSuccess() => testIife(gapi()); diff --git a/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/google_user.dart b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/google_user.dart new file mode 100644 index 000000000000..9f2b7b9bf6fa --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/google_user.dart @@ -0,0 +1,25 @@ +// 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:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; + +// Creates the JS representation of some user data +String googleUser(GoogleSignInUserData data) => ''' +{ + getBasicProfile: () => { + return { + getName: () => '${data.displayName}', + getEmail: () => '${data.email}', + getId: () => '${data.id}', + getImageUrl: () => '${data.photoUrl}', + }; + }, + getAuthResponse: () => { + return { + id_token: '${data.idToken}', + access_token: 'access_${data.idToken}', + } + }, +} +'''; diff --git a/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/test_iife.dart b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/test_iife.dart new file mode 100644 index 000000000000..43a7a044fc1b --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/test_iife.dart @@ -0,0 +1,15 @@ +// 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:google_sign_in_web/src/load_gapi.dart' + show kGapiOnloadCallbackFunctionName; + +// Wraps some JS mock code in an IIFE that ends by calling the onLoad dart callback. +String testIife(String mock) => ''' +(function() { + $mock; + window['$kGapiOnloadCallbackFunctionName'](); +})(); +''' + .replaceAll(RegExp(r'\s{2,}'), ''); diff --git a/packages/google_sign_in/google_sign_in_web/test/utils.dart b/packages/google_sign_in/google_sign_in_web/test/utils.dart new file mode 100644 index 000000000000..5a6c8906682c --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/test/utils.dart @@ -0,0 +1,10 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +String toBase64Url(String contents) { + // Open the file + return 'data:text/javascript;base64,' + base64.encode(utf8.encode(contents)); +} diff --git a/packages/image_picker/CHANGELOG.md b/packages/image_picker/CHANGELOG.md index 418d3a0a57d6..96bb4fcad8c3 100644 --- a/packages/image_picker/CHANGELOG.md +++ b/packages/image_picker/CHANGELOG.md @@ -1,3 +1,16 @@ +## 0.6.2+1 + +* Android: Fix a crash when a non-image file is picked. +* Android: Fix unwanted bitmap scaling. + +## 0.6.2 + +* iOS: Fixes an issue where picking content from Gallery would result in a crash on iOS 13. + +## 0.6.1+11 + +* Stability and Maintainability: update documentations, add unit tests. + ## 0.6.1+10 * iOS: Fix image orientation problems when scaling images. diff --git a/packages/image_picker/README.md b/packages/image_picker/README.md index 201113b2e771..fa73bfd1658a 100755 --- a/packages/image_picker/README.md +++ b/packages/image_picker/README.md @@ -5,8 +5,6 @@ A Flutter plugin for iOS and Android for picking images from the image library, and taking new pictures with the camera. -*Note*: This plugin is still under development, and some APIs might not be available yet. [Feedback welcome](https://github.com/flutter/flutter/issues) and [Pull Requests](https://github.com/flutter/plugins/pulls) are most welcome! - ## Installation First, add `image_picker` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). @@ -67,7 +65,7 @@ class _MyHomePageState extends State { ### Handling MainActivity destruction on Android -Android system -- although very rarely -- sometimes kills the MainActivity after the image_picker finishes. When this happens, we lost the data selected from the image_picker. You can use `retrieveLostData` to retrieve the lost data in this situation. For example: +Android system -- although very rarely -- sometimes kills the MainActivity after the image_picker finishes. When this happens, we lost the data selected from the image_picker. You can use `retrieveLostData` to retrieve the lost data in this situation. For example: ```dart Future retrieveLostData() async { diff --git a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index f9318e9c5760..e34a3b5632c0 100644 --- a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -521,10 +521,7 @@ private void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled if (methodCall != null) { Double maxWidth = methodCall.argument("maxWidth"); Double maxHeight = methodCall.argument("maxHeight"); - int imageQuality = - methodCall.argument("imageQuality") == null - ? 100 - : (int) methodCall.argument("imageQuality"); + Integer imageQuality = methodCall.argument("imageQuality"); String finalImagePath = imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality); diff --git a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java index ab3120afb6d0..ca0498bfa55b 100644 --- a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java +++ b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java @@ -7,6 +7,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.util.Log; +import androidx.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -28,31 +29,39 @@ class ImageResizer { *

If no resizing is needed, returns the path for the original image. */ String resizeImageIfNeeded( - String imagePath, Double maxWidth, Double maxHeight, int imageQuality) { + String imagePath, + @Nullable Double maxWidth, + @Nullable Double maxHeight, + @Nullable Integer imageQuality) { boolean shouldScale = - maxWidth != null || maxHeight != null || (imageQuality > -1 && imageQuality < 101); - - if (!shouldScale) { - return imagePath; + maxWidth != null || maxHeight != null || isImageQualityValid(imageQuality); + String[] pathParts = imagePath.split("/"); + String imageName = pathParts[pathParts.length - 1]; + File file; + Bitmap bmp = decodeFile(imagePath); + if (bmp == null) { + return null; } - try { - File scaledImage = resizedImage(imagePath, maxWidth, maxHeight, imageQuality); - exifDataCopier.copyExif(imagePath, scaledImage.getPath()); - - return scaledImage.getPath(); + if (!shouldScale) { + file = createImageOnExternalDirectory(imageName, bmp, 100); + } else { + file = resizedImage(bmp, maxWidth, maxHeight, imageQuality, imageName); + } + copyExif(imagePath, file.getPath()); + return file.getPath(); } catch (IOException e) { throw new RuntimeException(e); } } - private File resizedImage(String path, Double maxWidth, Double maxHeight, int imageQuality) + private File resizedImage( + Bitmap bmp, Double maxWidth, Double maxHeight, Integer imageQuality, String outputImageName) throws IOException { - Bitmap bmp = BitmapFactory.decodeFile(path); double originalWidth = bmp.getWidth() * 1.0; double originalHeight = bmp.getHeight() * 1.0; - if (imageQuality < 0 || imageQuality > 100) { + if (!isImageQualityValid(imageQuality)) { imageQuality = 100; } @@ -91,24 +100,51 @@ private File resizedImage(String path, Double maxWidth, Double maxHeight, int im } } - Bitmap scaledBmp = Bitmap.createScaledBitmap(bmp, width.intValue(), height.intValue(), false); + Bitmap scaledBmp = createScaledBitmap(bmp, width.intValue(), height.intValue(), false); + File file = + createImageOnExternalDirectory("/scaled_" + outputImageName, scaledBmp, imageQuality); + return file; + } + + private File createFile(File externalFilesDirectory, String child) { + return new File(externalFilesDirectory, child); + } + + private FileOutputStream createOutputStream(File imageFile) throws IOException { + return new FileOutputStream(imageFile); + } + + private void copyExif(String filePathOri, String filePathDest) { + exifDataCopier.copyExif(filePathOri, filePathDest); + } + + private Bitmap decodeFile(String path) { + return BitmapFactory.decodeFile(path); + } + + private Bitmap createScaledBitmap(Bitmap bmp, int width, int height, boolean filter) { + return Bitmap.createScaledBitmap(bmp, width, height, filter); + } + + private boolean isImageQualityValid(Integer imageQuality) { + return imageQuality != null && imageQuality > 0 && imageQuality < 100; + } + + private File createImageOnExternalDirectory(String name, Bitmap bitmap, int imageQuality) + throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - boolean saveAsPNG = bmp.hasAlpha(); + boolean saveAsPNG = bitmap.hasAlpha(); if (saveAsPNG) { Log.d( "ImageResizer", "image_picker: compressing is not supported for type PNG. Returning the image with original quality"); } - scaledBmp.compress( + bitmap.compress( saveAsPNG ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, imageQuality, outputStream); - - String[] pathParts = path.split("/"); - String imageName = pathParts[pathParts.length - 1]; - - File imageFile = new File(externalFilesDirectory, "/scaled_" + imageName); - FileOutputStream fileOutput = new FileOutputStream(imageFile); + File imageFile = createFile(externalFilesDirectory, name); + FileOutputStream fileOutput = createOutputStream(imageFile); fileOutput.write(outputStream.toByteArray()); fileOutput.close(); return imageFile; diff --git a/packages/image_picker/example/android/app/build.gradle b/packages/image_picker/example/android/app/build.gradle index 800e3e836a97..483268fdc18c 100755 --- a/packages/image_picker/example/android/app/build.gradle +++ b/packages/image_picker/example/android/app/build.gradle @@ -62,4 +62,5 @@ dependencies { testImplementation 'org.mockito:mockito-core:2.17.0' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + testImplementation "org.robolectric:robolectric:3.3.2" } diff --git a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java index 8e89a15abc8e..51733a503a92 100644 --- a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java +++ b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java @@ -25,10 +25,7 @@ import org.mockito.MockitoAnnotations; public class ImagePickerCacheTest { - private static final double WIDTH = 10.0; - private static final double HEIGHT = 10.0; private static final int IMAGE_QUALITY = 90; - private static final String PATH = "a_mock_path"; @Mock Activity mockActivity; @Mock SharedPreferences mockPreference; diff --git a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java index 60e1167cd87a..88fa3372766f 100644 --- a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java +++ b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java @@ -24,9 +24,9 @@ import org.mockito.MockitoAnnotations; public class ImagePickerDelegateTest { - private static final double WIDTH = 10.0; - private static final double HEIGHT = 10.0; - private static final int IMAGE_QUALITY = 100; + private static final Double WIDTH = 10.0; + private static final Double HEIGHT = 10.0; + private static final Integer IMAGE_QUALITY = 90; @Mock Activity mockActivity; @Mock ImageResizer mockImageResizer; @@ -62,13 +62,15 @@ public void setUp() { when(mockFileUtils.getPathFromUri(any(Context.class), any(Uri.class))) .thenReturn("pathFromUri"); + when(mockImageResizer.resizeImageIfNeeded("pathFromUri", null, null, null)) + .thenReturn("originalPath"); when(mockImageResizer.resizeImageIfNeeded("pathFromUri", null, null, IMAGE_QUALITY)) .thenReturn("originalPath"); - when(mockImageResizer.resizeImageIfNeeded("pathFromUri", WIDTH, HEIGHT, IMAGE_QUALITY)) + when(mockImageResizer.resizeImageIfNeeded("pathFromUri", WIDTH, HEIGHT, null)) .thenReturn("scaledPath"); - when(mockImageResizer.resizeImageIfNeeded("pathFromUri", WIDTH, null, IMAGE_QUALITY)) + when(mockImageResizer.resizeImageIfNeeded("pathFromUri", WIDTH, null, null)) .thenReturn("scaledPath"); - when(mockImageResizer.resizeImageIfNeeded("pathFromUri", null, HEIGHT, IMAGE_QUALITY)) + when(mockImageResizer.resizeImageIfNeeded("pathFromUri", null, HEIGHT, null)) .thenReturn("scaledPath"); mockFileUriResolver = new MockFileUriResolver(); diff --git a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java new file mode 100644 index 000000000000..aac7472eaac3 --- /dev/null +++ b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java @@ -0,0 +1,65 @@ +// Copyright 2019 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.imagepicker; + +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import java.io.File; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +// RobolectricTestRunner always creates a default mock bitmap when reading from file. So we cannot actually test the scaling. +// But we can still test whether the original or scaled file is created. +@RunWith(RobolectricTestRunner.class) +public class ImageResizerTest { + + ImageResizer resizer; + File imageFile; + File externalDirectory; + Bitmap originalImageBitmap; + + @Before + public void setUp() throws IOException { + MockitoAnnotations.initMocks(this); + imageFile = new File(getClass().getClassLoader().getResource("pngImage.png").getFile()); + originalImageBitmap = BitmapFactory.decodeFile(imageFile.getPath()); + TemporaryFolder temporaryFolder = new TemporaryFolder(); + temporaryFolder.create(); + externalDirectory = temporaryFolder.newFolder("image_picker_testing_path"); + resizer = new ImageResizer(externalDirectory, new ExifDataCopier()); + } + + @Test + public void onResizeImageIfNeeded_WhenQualityIsNull_ShoultNotResize_ReturnTheUnscaledFile() { + String outoutFile = resizer.resizeImageIfNeeded(imageFile.getPath(), null, null, null); + assertThat(outoutFile, equalTo(externalDirectory.getPath() + "/pngImage.png")); + } + + @Test + public void onResizeImageIfNeeded_WhenQualityIsNotNull_ShoulResize_ReturnResizedFile() { + String outoutFile = resizer.resizeImageIfNeeded(imageFile.getPath(), null, null, 50); + assertThat(outoutFile, equalTo(externalDirectory.getPath() + "/scaled_pngImage.png")); + } + + @Test + public void onResizeImageIfNeeded_WhenWidthIsNotNull_ShoulResize_ReturnResizedFile() { + String outoutFile = resizer.resizeImageIfNeeded(imageFile.getPath(), 50.0, null, null); + assertThat(outoutFile, equalTo(externalDirectory.getPath() + "/scaled_pngImage.png")); + } + + @Test + public void onResizeImageIfNeeded_WhenHeightIsNotNull_ShoulResize_ReturnResizedFile() { + String outoutFile = resizer.resizeImageIfNeeded(imageFile.getPath(), null, 50.0, null); + assertThat(outoutFile, equalTo(externalDirectory.getPath() + "/scaled_pngImage.png")); + } +} diff --git a/packages/image_picker/example/android/app/src/test/resources/pngImage.png b/packages/image_picker/example/android/app/src/test/resources/pngImage.png new file mode 100644 index 000000000000..22ac5a5a1485 Binary files /dev/null and b/packages/image_picker/example/android/app/src/test/resources/pngImage.png differ diff --git a/packages/image_picker/example/lib/main.dart b/packages/image_picker/example/lib/main.dart index a2175c098f17..b728788f5ba7 100755 --- a/packages/image_picker/example/lib/main.dart +++ b/packages/image_picker/example/lib/main.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: public_member_api_docs + import 'dart:async'; import 'dart:io'; diff --git a/packages/image_picker/ios/Classes/ImagePickerPlugin.m b/packages/image_picker/ios/Classes/ImagePickerPlugin.m index a23e13918148..5ac31a695a58 100644 --- a/packages/image_picker/ios/Classes/ImagePickerPlugin.m +++ b/packages/image_picker/ios/Classes/ImagePickerPlugin.m @@ -241,8 +241,30 @@ - (void)imagePickerController:(UIImagePickerController *)picker return; } if (videoURL != nil) { + if (@available(iOS 13.0, *)) { + NSString *fileName = [videoURL lastPathComponent]; + NSURL *destination = + [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]]; + + if ([[NSFileManager defaultManager] isReadableFileAtPath:[videoURL path]]) { + NSError *error; + if (![[videoURL path] isEqualToString:[destination path]]) { + [[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error]; + + if (error) { + self.result([FlutterError errorWithCode:@"flutter_image_picker_copy_video_error" + message:@"Could not cache the video file." + details:nil]); + self.result = nil; + return; + } + } + videoURL = destination; + } + } self.result(videoURL.path); self.result = nil; + } else { UIImage *image = [info objectForKey:UIImagePickerControllerEditedImage]; if (image == nil) { diff --git a/packages/image_picker/lib/image_picker.dart b/packages/image_picker/lib/image_picker.dart index bbe5157f4274..19cd0752ced3 100755 --- a/packages/image_picker/lib/image_picker.dart +++ b/packages/image_picker/lib/image_picker.dart @@ -8,7 +8,10 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +/// Denotes that an image is being picked. const String kTypeImage = 'image'; + +/// Denotes that a video is being picked. const String kTypeVideo = 'video'; /// Specifies the source where the picked image should come from. @@ -20,6 +23,8 @@ enum ImageSource { gallery, } +/// Provides an easy way to pick an image/video from the image library, +/// or to take a picture/video with the camera. class ImagePicker { static const MethodChannel _channel = MethodChannel('plugins.flutter.io/image_picker'); @@ -141,14 +146,17 @@ class ImagePicker { /// See also: /// * [ImagePicker.retrieveLostData] for more details on retrieving lost data. class LostDataResponse { + /// Creates an instance with the given [file], [exception], and [type]. Any of + /// the params may be null, but this is never considered to be empty. LostDataResponse({this.file, this.exception, this.type}); + /// Initializes an instance with all member params set to null and considered + /// to be empty. LostDataResponse.empty() : file = null, exception = null, - type = null { - _empty = true; - } + type = null, + _empty = true; /// Whether it is an empty response. /// @@ -176,4 +184,10 @@ class LostDataResponse { } /// The type of the retrieved data in a [LostDataResponse]. -enum RetrieveType { image, video } +enum RetrieveType { + /// A static picture. See [ImagePicker.pickImage]. + image, + + /// A video. See [ImagePicker.pickVideo]. + video +} diff --git a/packages/image_picker/pubspec.yaml b/packages/image_picker/pubspec.yaml index 93379d32fe7e..0e47c53e6911 100755 --- a/packages/image_picker/pubspec.yaml +++ b/packages/image_picker/pubspec.yaml @@ -5,7 +5,7 @@ authors: - Flutter Team - Rhodes Davis Jr. homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker -version: 0.6.1+10 +version: 0.6.2+1 flutter: plugin: diff --git a/packages/image_picker/test/image_picker_test.dart b/packages/image_picker/test/image_picker_test.dart index 1a40e11d76e1..28eae8eaa46d 100644 --- a/packages/image_picker/test/image_picker_test.dart +++ b/packages/image_picker/test/image_picker_test.dart @@ -143,6 +143,33 @@ void main() { }); }); + group('#pickVideo', () { + test('passes the image source argument correctly', () async { + await ImagePicker.pickVideo(source: ImageSource.camera); + await ImagePicker.pickVideo(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 1, + }), + ], + ); + }); + + test('handles a null image path response gracefully', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect( + await ImagePicker.pickVideo(source: ImageSource.gallery), isNull); + expect(await ImagePicker.pickVideo(source: ImageSource.camera), isNull); + }); + }); + group('#retrieveLostData', () { test('retrieveLostData get success response', () async { channel.setMockMethodCallHandler((MethodCall methodCall) async { diff --git a/packages/in_app_purchase/analysis_options.yaml b/packages/in_app_purchase/analysis_options.yaml index afa04c8cb084..6a095d6e3a69 100644 --- a/packages/in_app_purchase/analysis_options.yaml +++ b/packages/in_app_purchase/analysis_options.yaml @@ -1,3 +1,16 @@ +# This is a temporary file to allow us to land a new set of linter rules in a +# series of manageable patches instead of one gigantic PR. It disables some of +# the new lints that are already failing on this plugin, for this plugin. It +# should be deleted and the failing lints addressed as soon as possible. + +include: ../../analysis_options.yaml + analyzer: - exclude: - - lib/**/*.g.dart # Ignore generated files \ No newline at end of file + errors: + avoid_init_to_null: ignore + prefer_is_empty: ignore + prefer_is_not_empty: ignore + public_member_api_docs: ignore + type_init_formals: ignore + unnecessary_new: ignore + unawaited_futures: ignore diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart index 32f8fc79681e..d64467ed3775 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart @@ -186,8 +186,10 @@ class GooglePlayConnection } on PlatformException catch (e) { exception = e; responses = [ + // ignore: invalid_use_of_visible_for_testing_member SkuDetailsResponseWrapper( responseCode: BillingResponse.error, skuDetailsList: []), + // ignore: invalid_use_of_visible_for_testing_member SkuDetailsResponseWrapper( responseCode: BillingResponse.error, skuDetailsList: []) ]; diff --git a/packages/local_auth/example/lib/main.dart b/packages/local_auth/example/lib/main.dart index 26dccb750289..06e33b9853be 100644 --- a/packages/local_auth/example/lib/main.dart +++ b/packages/local_auth/example/lib/main.dart @@ -1,6 +1,9 @@ // Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + import 'dart:async'; import 'package:flutter/material.dart'; diff --git a/packages/local_auth/lib/auth_strings.dart b/packages/local_auth/lib/auth_strings.dart index 26646bbf41b4..a8f34f88723c 100644 --- a/packages/local_auth/lib/auth_strings.dart +++ b/packages/local_auth/lib/auth_strings.dart @@ -2,6 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// This is a temporary ignore to allow us to land a new set of linter rules in a +// series of manageable patches instead of one gigantic PR. It disables some of +// the new lints that are already failing on this plugin, for this plugin. It +// should be deleted and the failing lints addressed as soon as possible. +// ignore_for_file: public_member_api_docs + import 'package:intl/intl.dart'; /// Android side authentication messages. diff --git a/packages/local_auth/lib/local_auth.dart b/packages/local_auth/lib/local_auth.dart index 31c1a41d0266..b2b03b920d64 100644 --- a/packages/local_auth/lib/local_auth.dart +++ b/packages/local_auth/lib/local_auth.dart @@ -2,6 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// This is a temporary ignore to allow us to land a new set of linter rules in a +// series of manageable patches instead of one gigantic PR. It disables some of +// the new lints that are already failing on this plugin, for this plugin. It +// should be deleted and the failing lints addressed as soon as possible. +// ignore_for_file: public_member_api_docs + import 'dart:async'; import 'package:flutter/services.dart'; diff --git a/packages/package_info/analysis_options.yaml b/packages/package_info/analysis_options.yaml new file mode 100644 index 000000000000..d4ccef63f1d1 --- /dev/null +++ b/packages/package_info/analysis_options.yaml @@ -0,0 +1,11 @@ +# This is a temporary file to allow us to land a new set of linter rules in a +# series of manageable patches instead of one gigantic PR. It disables some of +# the new lints that are already failing on this plugin, for this plugin. It +# should be deleted and the failing lints addressed as soon as possible. + +include: ../../analysis_options.yaml + +analyzer: + errors: + public_member_api_docs: ignore + unawaited_futures: ignore diff --git a/packages/path_provider/CHANGELOG.md b/packages/path_provider/CHANGELOG.md index 4c4dca44220e..505977e469cf 100644 --- a/packages/path_provider/CHANGELOG.md +++ b/packages/path_provider/CHANGELOG.md @@ -1,3 +1,22 @@ +## 1.4.5 + +* Add support for v2 plugins APIs. + +## 1.4.4 + +* Update driver tests in the example app to e2e tests. + +## 1.4.3 + +* Update driver tests in the example app to e2e tests. +* Add missing DartDocs and a lint to prevent further regressions. + +## 1.4.2 + +* Update and migrate iOS example project by removing flutter_assets, change + "English" to "en", remove extraneous xcconfigs, update to Xcode 11 build + settings, remove ARCHS, and build pods as libraries instead of frameworks. + ## 1.4.1 * Remove AndroidX warnings. diff --git a/packages/path_provider/android/build.gradle b/packages/path_provider/android/build.gradle index 93460e761568..3bc008e63e0c 100644 --- a/packages/path_provider/android/build.gradle +++ b/packages/path_provider/android/build.gradle @@ -37,3 +37,29 @@ dependencies { implementation 'androidx.annotation:annotation:1.1.0' testImplementation 'junit:junit:4.12' } + +// TODO(cyanglaz): Remove this hack once androidx.lifecycle is included on stable. https://github.com/flutter/flutter/issues/42348 +afterEvaluate { + def containsEmbeddingDependencies = false + for (def configuration : configurations.all) { + for (def dependency : configuration.dependencies) { + if (dependency.group == 'io.flutter' && + dependency.name.startsWith('flutter_embedding') && + dependency.isTransitive()) + { + containsEmbeddingDependencies = true + break + } + } + } + if (!containsEmbeddingDependencies) { + android { + dependencies { + def lifecycle_version = "1.1.1" + compileOnly "android.arch.lifecycle:runtime:$lifecycle_version" + compileOnly "android.arch.lifecycle:common:$lifecycle_version" + compileOnly "android.arch.lifecycle:common-java8:$lifecycle_version" + } + } + } +} diff --git a/packages/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java b/packages/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java index 30b00d0e5532..0bf9ee4820a3 100644 --- a/packages/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java +++ b/packages/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java @@ -4,9 +4,11 @@ package io.flutter.plugins.pathprovider; +import android.content.Context; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import androidx.annotation.NonNull; +import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -17,19 +19,33 @@ import java.util.ArrayList; import java.util.List; -public class PathProviderPlugin implements MethodCallHandler { +public class PathProviderPlugin implements FlutterPlugin, MethodCallHandler { - private final Registrar mRegistrar; + private Context context; + private MethodChannel channel; + + public PathProviderPlugin() {} public static void registerWith(Registrar registrar) { - MethodChannel channel = - new MethodChannel(registrar.messenger(), "plugins.flutter.io/path_provider"); - PathProviderPlugin instance = new PathProviderPlugin(registrar); - channel.setMethodCallHandler(instance); + PathProviderPlugin instance = new PathProviderPlugin(); + instance.channel = new MethodChannel(registrar.messenger(), "plugins.flutter.io/path_provider"); + instance.context = registrar.context(); + instance.channel.setMethodCallHandler(instance); } - private PathProviderPlugin(Registrar registrar) { - this.mRegistrar = registrar; + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + channel = + new MethodChannel( + binding.getFlutterEngine().getDartExecutor(), "plugins.flutter.io/path_provider"); + context = binding.getApplicationContext(); + channel.setMethodCallHandler(this); + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + channel.setMethodCallHandler(null); + channel = null; } @Override @@ -61,19 +77,19 @@ public void onMethodCall(MethodCall call, @NonNull Result result) { } private String getPathProviderTemporaryDirectory() { - return mRegistrar.context().getCacheDir().getPath(); + return context.getCacheDir().getPath(); } private String getApplicationSupportDirectory() { - return PathUtils.getFilesDir(mRegistrar.context()); + return PathUtils.getFilesDir(context); } private String getPathProviderApplicationDocumentsDirectory() { - return PathUtils.getDataDirectory(mRegistrar.context()); + return PathUtils.getDataDirectory(context); } private String getPathProviderStorageDirectory() { - final File dir = mRegistrar.context().getExternalFilesDir(null); + final File dir = context.getExternalFilesDir(null); if (dir == null) { return null; } @@ -84,13 +100,13 @@ private List getPathProviderExternalCacheDirectories() { final List paths = new ArrayList<>(); if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { - for (File dir : mRegistrar.context().getExternalCacheDirs()) { + for (File dir : context.getExternalCacheDirs()) { if (dir != null) { paths.add(dir.getAbsolutePath()); } } } else { - File dir = mRegistrar.context().getExternalCacheDir(); + File dir = context.getExternalCacheDir(); if (dir != null) { paths.add(dir.getAbsolutePath()); } @@ -103,13 +119,13 @@ private List getPathProviderExternalStorageDirectories(String type) { final List paths = new ArrayList<>(); if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { - for (File dir : mRegistrar.context().getExternalFilesDirs(type)) { + for (File dir : context.getExternalFilesDirs(type)) { if (dir != null) { paths.add(dir.getAbsolutePath()); } } } else { - File dir = mRegistrar.context().getExternalFilesDir(type); + File dir = context.getExternalFilesDir(type); if (dir != null) { paths.add(dir.getAbsolutePath()); } diff --git a/packages/path_provider/example/android/app/build.gradle b/packages/path_provider/example/android/app/build.gradle index 2ca6a7a4add3..0404c7203903 100644 --- a/packages/path_provider/example/android/app/build.gradle +++ b/packages/path_provider/example/android/app/build.gradle @@ -54,6 +54,10 @@ flutter { } dependencies { + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test:rules:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' diff --git a/packages/path_provider/example/android/app/src/androidTest/java/EmbeddingV1ActivityTest.java b/packages/path_provider/example/android/app/src/androidTest/java/EmbeddingV1ActivityTest.java new file mode 100644 index 000000000000..cce04b79f516 --- /dev/null +++ b/packages/path_provider/example/android/app/src/androidTest/java/EmbeddingV1ActivityTest.java @@ -0,0 +1,15 @@ + +package io.flutter.plugins.pathprovider; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.e2e.FlutterRunner; +import io.flutter.plugins.pathproviderexample.EmbeddingV1Activity; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterRunner.class) +public class EmbeddingV1ActivityTest { + @Rule + public ActivityTestRule rule = + new ActivityTestRule<>(EmbeddingV1Activity.class); +} diff --git a/packages/path_provider/example/android/app/src/androidTest/java/MainActivityTest.java b/packages/path_provider/example/android/app/src/androidTest/java/MainActivityTest.java new file mode 100644 index 000000000000..7bdd449981f5 --- /dev/null +++ b/packages/path_provider/example/android/app/src/androidTest/java/MainActivityTest.java @@ -0,0 +1,13 @@ + +package io.flutter.plugins.pathprovider; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.e2e.FlutterRunner; +import io.flutter.plugins.pathproviderexample.MainActivity; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterRunner.class) +public class MainActivityTest { + @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); +} diff --git a/packages/path_provider/example/android/app/src/main/AndroidManifest.xml b/packages/path_provider/example/android/app/src/main/AndroidManifest.xml index 793c13b6e612..9e03a9373e33 100644 --- a/packages/path_provider/example/android/app/src/main/AndroidManifest.xml +++ b/packages/path_provider/example/android/app/src/main/AndroidManifest.xml @@ -4,6 +4,12 @@ + + /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"; + 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 */ @@ -335,19 +318,28 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -383,19 +375,28 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -424,7 +425,6 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { - ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -446,7 +446,6 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/packages/path_provider/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/path_provider/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1c9580788197..3bb3697ef41c 100644 --- a/packages/path_provider/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/path_provider/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + - - allTestsCompleter = Completer(); - enableFlutterDriverExtension(handler: (_) => allTestsCompleter.future); - tearDownAll(() => allTestsCompleter.complete(null)); + E2EWidgetsFlutterBinding.ensureInitialized(); - test('getTemporaryDirectory', () async { + testWidgets('getTemporaryDirectory', (WidgetTester tester) async { final Directory result = await getTemporaryDirectory(); _verifySampleFile(result, 'temporaryDirectory'); }); - test('getApplicationDocumentsDirectory', () async { + testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { final Directory result = await getApplicationDocumentsDirectory(); _verifySampleFile(result, 'applicationDocuments'); }); - test('getApplicationSupportDirectory', () async { + testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { final Directory result = await getApplicationSupportDirectory(); _verifySampleFile(result, 'applicationSupport'); }); - test('getLibraryDirectory', () async { + testWidgets('getLibraryDirectory', (WidgetTester tester) async { if (Platform.isIOS) { final Directory result = await getLibraryDirectory(); _verifySampleFile(result, 'library'); @@ -39,7 +37,7 @@ void main() { } }); - test('getExternalStorageDirectory', () async { + testWidgets('getExternalStorageDirectory', (WidgetTester tester) async { if (Platform.isIOS) { final Future result = getExternalStorageDirectory(); expect(result, throwsA(isInstanceOf())); @@ -49,7 +47,7 @@ void main() { } }); - test('getExternalCacheDirectories', () async { + testWidgets('getExternalCacheDirectories', (WidgetTester tester) async { if (Platform.isIOS) { final Future> result = getExternalCacheDirectories(); expect(result, throwsA(isInstanceOf())); diff --git a/packages/google_maps_flutter/example/test_driver/google_maps_test.dart b/packages/path_provider/example/test_driver/path_provider_e2e_test.dart similarity index 68% rename from packages/google_maps_flutter/example/test_driver/google_maps_test.dart rename to packages/path_provider/example/test_driver/path_provider_e2e_test.dart index b0d3305cd652..f3aa9e218d82 100644 --- a/packages/google_maps_flutter/example/test_driver/google_maps_test.dart +++ b/packages/path_provider/example/test_driver/path_provider_e2e_test.dart @@ -3,11 +3,13 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; - +import 'dart:io'; import 'package:flutter_driver/flutter_driver.dart'; Future main() async { final FlutterDriver driver = await FlutterDriver.connect(); - await driver.requestData(null, timeout: const Duration(minutes: 1)); - driver.close(); + final String result = + await driver.requestData(null, timeout: const Duration(minutes: 1)); + await driver.close(); + exit(result == 'pass' ? 0 : 1); } diff --git a/packages/path_provider/lib/path_provider.dart b/packages/path_provider/lib/path_provider.dart index 3840121c8c4b..52aceb786477 100644 --- a/packages/path_provider/lib/path_provider.dart +++ b/packages/path_provider/lib/path_provider.dart @@ -14,6 +14,8 @@ const MethodChannel _channel = Platform _platform = const LocalPlatform(); +/// This API is only exposed for the unit tests. It should not be used by +/// any code outside of the plugin itself. @visibleForTesting void setMockPathProviderPlatform(Platform platform) { _platform = platform; @@ -141,15 +143,49 @@ Future> getExternalCacheDirectories() async { /// /// https://developer.android.com/reference/android/os/Environment.html#fields_1 enum StorageDirectory { + /// Contains audio files that should be treated as music. + /// + /// See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_MUSIC. music, + + /// Contains audio files that should be treated as podcasts. + /// + /// See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_PODCASTS. podcasts, + + /// Contains audio files that should be treated as ringtones. + /// + /// See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_RINGTONES. ringtones, + + /// Contains audio files that should be treated as alarm sounds. + /// + /// See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_ALARMS. alarms, + + /// Contains audio files that should be treated as notification sounds. + /// + /// See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_NOTIFICATIONS. notifications, + + /// Contains images. See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_PICTURES. pictures, + + /// Contains movies. See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_MOVIES. movies, + + /// Contains files of any type that have been downloaded by the user. + /// + /// See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_DOWNLOADS. downloads, + + /// Used to hold both pictures and videos when the device filesystem is + /// treated like a camera's. + /// + /// See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_DCIM. dcim, + + /// Holds user-created documents. See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_DOCUMENTS. documents, } diff --git a/packages/path_provider/pubspec.yaml b/packages/path_provider/pubspec.yaml index ae4bae6f9c8c..4bbd4fc065ea 100644 --- a/packages/path_provider/pubspec.yaml +++ b/packages/path_provider/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for getting commonly used locations on the Android & iOS file systems, such as the temp and app data directories. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider -version: 1.4.1 +version: 1.4.5 flutter: plugin: @@ -18,6 +18,7 @@ dependencies: meta: ^1.0.5 dev_dependencies: + e2e: ^0.2.1 flutter_test: sdk: flutter flutter_driver: @@ -27,4 +28,4 @@ dev_dependencies: environment: sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=0.1.4 <2.0.0" + flutter: ">=1.9.1+hotfix.5 <2.0.0" diff --git a/packages/path_provider/test/path_provider_e2e.dart b/packages/path_provider/test/path_provider_e2e.dart new file mode 100644 index 000000000000..545671e32b01 --- /dev/null +++ b/packages/path_provider/test/path_provider_e2e.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:e2e/e2e.dart'; + +void main() { + E2EWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can get temporary directory', (WidgetTester tester) async { + final String tempPath = (await getTemporaryDirectory()).path; + expect(tempPath, isNotEmpty); + }); +} diff --git a/packages/quick_actions/CHANGELOG.md b/packages/quick_actions/CHANGELOG.md index 0069c0c5165e..2c9aea75ff0d 100644 --- a/packages/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/CHANGELOG.md @@ -1,3 +1,24 @@ +## 0.4.0 + +- Added missing documentation. +- **Breaking change**. `channel` and `withMethodChannel` are now + `@visibleForTesting`. These methods are for plugin unit tests only and may be + removed in the future. +- **Breaking change**. Removed `runLaunchAction` from public API. This method + was not meant to be used by consumers of the plugin. + +## 0.3.3+1 + +* Update and migrate iOS example project by removing flutter_assets, change + "English" to "en", remove extraneous xcconfigs, update to Xcode 11 build + settings, and remove ARCHS and DEVELOPMENT_TEAM. + +## 0.3.3 + +* Support Android V2 embedding. +* Add e2e tests. +* Migrate to using the new e2e test binding. + ## 0.3.2+4 * Remove AndroidX warnings. @@ -7,6 +28,7 @@ * Define clang module for iOS. ## 0.3.2+2 + * Fix bug that would make the shortcut not open on Android. * Report shortcut used on Android. * Improves example. @@ -17,7 +39,7 @@ ## 0.3.2 -* Fixed the quick actions launch on Android when the app is killed. +* Fixed the quick actions launch on Android when the app is killed. ## 0.3.1 diff --git a/packages/quick_actions/android/build.gradle b/packages/quick_actions/android/build.gradle index 648b654dbfcd..d474a71c7447 100644 --- a/packages/quick_actions/android/build.gradle +++ b/packages/quick_actions/android/build.gradle @@ -32,3 +32,28 @@ android { disable 'InvalidPackage' } } + +afterEvaluate { + def containsEmbeddingDependencies = false + for (def configuration : configurations.all) { + for (def dependency : configuration.dependencies) { + if (dependency.group == 'io.flutter' && + dependency.name.startsWith('flutter_embedding') && + dependency.isTransitive()) + { + containsEmbeddingDependencies = true + break + } + } + } + if (!containsEmbeddingDependencies) { + android { + dependencies { + def lifecycle_version = "1.1.1" + compileOnly "android.arch.lifecycle:runtime:$lifecycle_version" + compileOnly "android.arch.lifecycle:common:$lifecycle_version" + compileOnly "android.arch.lifecycle:common-java8:$lifecycle_version" + } + } + } +} diff --git a/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java b/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java new file mode 100644 index 000000000000..dcf2390570bd --- /dev/null +++ b/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java @@ -0,0 +1,131 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.quickactions; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.content.res.Resources; +import android.graphics.drawable.Icon; +import android.os.Build; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { + + private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions"; + private static final String EXTRA_ACTION = "some unique action key"; + + private final Context context; + private Activity activity; + + MethodCallHandlerImpl(Context context, Activity activity) { + this.context = context; + this.activity = activity; + } + + void setActivity(Activity activity) { + this.activity = activity; + } + + @Override + public void onMethodCall(MethodCall call, MethodChannel.Result result) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + // We already know that this functionality does not work for anything + // lower than API 25 so we chose not to return error. Instead we do nothing. + result.success(null); + return; + } + ShortcutManager shortcutManager = + (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); + switch (call.method) { + case "setShortcutItems": + List> serializedShortcuts = call.arguments(); + List shortcuts = deserializeShortcuts(serializedShortcuts); + shortcutManager.setDynamicShortcuts(shortcuts); + break; + case "clearShortcutItems": + shortcutManager.removeAllDynamicShortcuts(); + break; + case "getLaunchAction": + if (activity == null) { + result.error( + "quick_action_getlaunchaction_no_activity", + "There is no activity available when launching action", + null); + return; + } + final Intent intent = activity.getIntent(); + final String launchAction = intent.getStringExtra(EXTRA_ACTION); + if (launchAction != null && !launchAction.isEmpty()) { + shortcutManager.reportShortcutUsed(launchAction); + intent.removeExtra(EXTRA_ACTION); + } + result.success(launchAction); + return; + default: + result.notImplemented(); + return; + } + result.success(null); + } + + @TargetApi(Build.VERSION_CODES.N_MR1) + private List deserializeShortcuts(List> shortcuts) { + final List shortcutInfos = new ArrayList<>(); + + for (Map shortcut : shortcuts) { + final String icon = shortcut.get("icon"); + final String type = shortcut.get("type"); + final String title = shortcut.get("localizedTitle"); + final ShortcutInfo.Builder shortcutBuilder = new ShortcutInfo.Builder(context, type); + + final int resourceId = loadResourceId(context, icon); + final Intent intent = getIntentToOpenMainActivity(type); + + if (resourceId > 0) { + shortcutBuilder.setIcon(Icon.createWithResource(context, resourceId)); + } + + final ShortcutInfo shortcutInfo = + shortcutBuilder.setLongLabel(title).setShortLabel(title).setIntent(intent).build(); + shortcutInfos.add(shortcutInfo); + } + return shortcutInfos; + } + + private int loadResourceId(Context context, String icon) { + if (icon == null) { + return 0; + } + final String packageName = context.getPackageName(); + final Resources res = context.getResources(); + final int resourceId = res.getIdentifier(icon, "drawable", packageName); + + if (resourceId == 0) { + return res.getIdentifier(icon, "mipmap", packageName); + } else { + return resourceId; + } + } + + private Intent getIntentToOpenMainActivity(String type) { + final String packageName = context.getPackageName(); + + return context + .getPackageManager() + .getLaunchIntentForPackage(packageName) + .setAction(Intent.ACTION_RUN) + .putExtra(EXTRA_ACTION, type) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + } +} diff --git a/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java b/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java index 3a4ba2410666..76285fb90d10 100644 --- a/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java +++ b/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java @@ -4,33 +4,21 @@ package io.flutter.plugins.quickactions; -import android.annotation.TargetApi; +import android.app.Activity; import android.content.Context; -import android.content.Intent; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; -import android.content.res.Resources; -import android.graphics.drawable.Icon; -import android.os.Build; -import io.flutter.plugin.common.MethodCall; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry.Registrar; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; /** QuickActionsPlugin */ -public class QuickActionsPlugin implements MethodCallHandler { +public class QuickActionsPlugin implements FlutterPlugin, ActivityAware { private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions"; - private static final String EXTRA_ACTION = "some unique action key"; - private final Registrar registrar; - - private QuickActionsPlugin(Registrar registrar) { - this.registrar = registrar; - } + private MethodChannel channel; + private MethodCallHandlerImpl handler; /** * Plugin registration. @@ -38,96 +26,50 @@ private QuickActionsPlugin(Registrar registrar) { *

Must be called when the application is created. */ public static void registerWith(Registrar registrar) { - final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_ID); - channel.setMethodCallHandler(new QuickActionsPlugin(registrar)); + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + plugin.setupChannel(registrar.messenger(), registrar.context(), registrar.activity()); } @Override - public void onMethodCall(MethodCall call, Result result) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { - // We already know that this functionality does not work for anything - // lower than API 25 so we chose not to return error. Instead we do nothing. - result.success(null); - return; - } - Context context = registrar.context(); - ShortcutManager shortcutManager = - (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); - switch (call.method) { - case "setShortcutItems": - List> serializedShortcuts = call.arguments(); - List shortcuts = deserializeShortcuts(serializedShortcuts); - shortcutManager.setDynamicShortcuts(shortcuts); - break; - case "clearShortcutItems": - shortcutManager.removeAllDynamicShortcuts(); - break; - case "getLaunchAction": - final Intent intent = registrar.activity().getIntent(); - final String launchAction = intent.getStringExtra(EXTRA_ACTION); - if (launchAction != null && !launchAction.isEmpty()) { - shortcutManager.reportShortcutUsed(launchAction); - intent.removeExtra(EXTRA_ACTION); - } - result.success(launchAction); - return; - default: - result.notImplemented(); - return; - } - result.success(null); + public void onAttachedToEngine(FlutterPluginBinding binding) { + setupChannel( + binding.getFlutterEngine().getDartExecutor(), binding.getApplicationContext(), null); } - @TargetApi(Build.VERSION_CODES.N_MR1) - private List deserializeShortcuts(List> shortcuts) { - final List shortcutInfos = new ArrayList<>(); - final Context context = registrar.context(); - - for (Map shortcut : shortcuts) { - final String icon = shortcut.get("icon"); - final String type = shortcut.get("type"); - final String title = shortcut.get("localizedTitle"); - final ShortcutInfo.Builder shortcutBuilder = new ShortcutInfo.Builder(context, type); - - final int resourceId = loadResourceId(context, icon); - final Intent intent = getIntentToOpenMainActivity(type); + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) { + teardownChannel(); + } - if (resourceId > 0) { - shortcutBuilder.setIcon(Icon.createWithResource(context, resourceId)); - } + @Override + public void onAttachedToActivity(ActivityPluginBinding binding) { + handler.setActivity(binding.getActivity()); + } - final ShortcutInfo shortcutInfo = - shortcutBuilder.setLongLabel(title).setShortLabel(title).setIntent(intent).build(); - shortcutInfos.add(shortcutInfo); - } - return shortcutInfos; + @Override + public void onDetachedFromActivity() { + handler.setActivity(null); } - private int loadResourceId(Context context, String icon) { - if (icon == null) { - return 0; - } - final String packageName = context.getPackageName(); - final Resources res = context.getResources(); - final int resourceId = res.getIdentifier(icon, "drawable", packageName); + @Override + public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + onAttachedToActivity(binding); + } - if (resourceId == 0) { - return res.getIdentifier(icon, "mipmap", packageName); - } else { - return resourceId; - } + @Override + public void onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity(); } - private Intent getIntentToOpenMainActivity(String type) { - final Context context = registrar.context(); - final String packageName = context.getPackageName(); + private void setupChannel(BinaryMessenger messenger, Context context, Activity activity) { + channel = new MethodChannel(messenger, CHANNEL_ID); + handler = new MethodCallHandlerImpl(context, activity); + channel.setMethodCallHandler(handler); + } - return context - .getPackageManager() - .getLaunchIntentForPackage(packageName) - .setAction(Intent.ACTION_RUN) - .putExtra(EXTRA_ACTION, type) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + private void teardownChannel() { + channel.setMethodCallHandler(null); + channel = null; + handler = null; } } diff --git a/packages/quick_actions/example/android/app/src/main/AndroidManifest.xml b/packages/quick_actions/example/android/app/src/main/AndroidManifest.xml index bb7a1351d343..6c70f1b650cd 100644 --- a/packages/quick_actions/example/android/app/src/main/AndroidManifest.xml +++ b/packages/quick_actions/example/android/app/src/main/AndroidManifest.xml @@ -6,15 +6,22 @@ - + android:windowSoftInputMode="adjustResize" + android:exported="true"> + + diff --git a/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1Activity.java similarity index 80% rename from packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java rename to packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1Activity.java index 133c3fa2c898..ba6a64d997bc 100644 --- a/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java +++ b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1Activity.java @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package io.flutter.plugins.videoplayerexample; +package io.flutter.plugins.quickactionsexample; import android.os.Bundle; import io.flutter.app.FlutterActivity; import io.flutter.plugins.GeneratedPluginRegistrant; -public class MainActivity extends FlutterActivity { +public class EmbeddingV1Activity extends FlutterActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1ActivityTest.java b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1ActivityTest.java new file mode 100644 index 000000000000..b3e2a08c44b4 --- /dev/null +++ b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1ActivityTest.java @@ -0,0 +1,17 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.quickactionsexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.e2e.FlutterRunner; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterRunner.class) +public class EmbeddingV1ActivityTest { + @Rule + public ActivityTestRule rule = + new ActivityTestRule<>(EmbeddingV1Activity.class); +} diff --git a/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/MainActivity.java b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/MainActivity.java index 74e3eb873cbc..ce128b60648e 100644 --- a/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/MainActivity.java +++ b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/MainActivity.java @@ -1,17 +1,20 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2019 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package io.flutter.plugins.quickactionsexample; -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugins.quickactions.QuickActionsPlugin; public class MainActivity extends FlutterActivity { + + // TODO(cyanglaz): Remove this once v2 of GeneratedPluginRegistrant rolls to stable. + // https://github.com/flutter/flutter/issues/42694 @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); + public void configureFlutterEngine(FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + flutterEngine.getPlugins().add(new QuickActionsPlugin()); } } diff --git a/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/MainActivityTest.java b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/MainActivityTest.java new file mode 100644 index 000000000000..4d3e1822d616 --- /dev/null +++ b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/MainActivityTest.java @@ -0,0 +1,15 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.quickactionsexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.e2e.FlutterRunner; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterRunner.class) +public class MainActivityTest { + @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); +} diff --git a/packages/quick_actions/example/android/gradle.properties b/packages/quick_actions/example/android/gradle.properties index 8bd86f680510..38c8d4544ff1 100644 --- a/packages/quick_actions/example/android/gradle.properties +++ b/packages/quick_actions/example/android/gradle.properties @@ -1 +1,4 @@ org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj b/packages/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj index 9d6dc0f30f21..fdd275fcede5 100644 --- a/packages/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,15 +8,12 @@ /* 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, ); }; }; 83C36CAF23D629E5ABE75B2A /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */; }; 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 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 */; }; @@ -42,9 +39,9 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 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 = ""; }; 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 5278439583922091276A37C9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -58,6 +55,7 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F0609304FBCAEC2289164BD5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -79,7 +77,6 @@ children = ( 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -142,6 +139,8 @@ D0FE95BE2380323DD75CB891 /* Pods */ = { isa = PBXGroup; children = ( + 5278439583922091276A37C9 /* Pods-Runner.debug.xcconfig */, + F0609304FBCAEC2289164BD5 /* Pods-Runner.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -161,7 +160,6 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, FEDDF02AA7C2BA0D1905BD95 /* [CP] Embed Pods Frameworks */, - BEA76C3BEB02665DE83A6355 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -178,18 +176,17 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0830; + LastUpgradeCheck = 1100; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = JSJA5AH6K6; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -211,10 +208,7 @@ buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); @@ -251,21 +245,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - BEA76C3BEB02665DE83A6355 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; C6989ECD8FF0836301D734B4 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -290,16 +269,13 @@ files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/.symlinks/flutter/ios/Flutter.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -342,19 +318,28 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -390,19 +375,28 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -431,9 +425,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { - ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = JSJA5AH6K6; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -454,9 +446,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = JSJA5AH6K6; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/packages/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1c9580788197..3bb3697ef41c 100644 --- a/packages/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + - - =2.0.0-dev.28.0 <3.0.0" + flutter: ">=1.9.1+hotfix.2 <2.0.0" diff --git a/packages/quick_actions/example/test_driver/quick_actions_e2e.dart b/packages/quick_actions/example/test_driver/quick_actions_e2e.dart new file mode 100644 index 000000000000..41d35b874640 --- /dev/null +++ b/packages/quick_actions/example/test_driver/quick_actions_e2e.dart @@ -0,0 +1,24 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:e2e/e2e.dart'; +import 'package:quick_actions/quick_actions.dart'; + +void main() { + E2EWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can set shortcuts', (WidgetTester tester) async { + final QuickActions quickActions = QuickActions(); + quickActions.initialize(null); + + const ShortcutItem shortCutItem = ShortcutItem( + type: 'action_one', + localizedTitle: 'Action one', + icon: 'AppIcon', + ); + expect( + quickActions.setShortcutItems([shortCutItem]), completes); + }); +} diff --git a/packages/quick_actions/example/test_driver/quick_actions_e2e_test.dart b/packages/quick_actions/example/test_driver/quick_actions_e2e_test.dart new file mode 100644 index 000000000000..f3aa9e218d82 --- /dev/null +++ b/packages/quick_actions/example/test_driver/quick_actions_e2e_test.dart @@ -0,0 +1,15 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; +import 'package:flutter_driver/flutter_driver.dart'; + +Future main() async { + final FlutterDriver driver = await FlutterDriver.connect(); + final String result = + await driver.requestData(null, timeout: const Duration(minutes: 1)); + await driver.close(); + exit(result == 'pass' ? 0 : 1); +} diff --git a/packages/quick_actions/lib/quick_actions.dart b/packages/quick_actions/lib/quick_actions.dart index f240968eb8f5..933162a1a47c 100644 --- a/packages/quick_actions/lib/quick_actions.dart +++ b/packages/quick_actions/lib/quick_actions.dart @@ -17,6 +17,10 @@ typedef void QuickActionHandler(String type); /// Home screen quick-action shortcut item. class ShortcutItem { + /// Constructs an instance with the given [type], [localizedTitle], and + /// [icon]. + /// + /// Only [icon] should be nullable. It will remain `null` if unset. const ShortcutItem({ @required this.type, @required this.localizedTitle, @@ -36,14 +40,21 @@ class ShortcutItem { /// Quick actions plugin. class QuickActions { + /// Gets an instance of the plugin with the default methodChannel. + /// + /// [initialize] should be called before using any other methods. factory QuickActions() => _instance; + /// This is a test-only constructor. Do not call this, it can break at any + /// time. @visibleForTesting QuickActions.withMethodChannel(this.channel); static final QuickActions _instance = QuickActions.withMethodChannel(_kChannel); + /// This is a test-only accessor. Do not call this, it can break at any time. + @visibleForTesting final MethodChannel channel; /// Initializes this plugin. @@ -54,10 +65,6 @@ class QuickActions { assert(call.method == 'launch'); handler(call.arguments); }); - runLaunchAction(handler); - } - - void runLaunchAction(QuickActionHandler handler) async { final String action = await channel.invokeMethod('getLaunchAction'); if (action != null) { handler(action); diff --git a/packages/quick_actions/pubspec.yaml b/packages/quick_actions/pubspec.yaml index da725aa55790..ab8832865e5d 100644 --- a/packages/quick_actions/pubspec.yaml +++ b/packages/quick_actions/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for creating shortcuts on home screen, also known as Quick Actions on iOS and App Shortcuts on Android. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/quick_actions -version: 0.3.2+4 +version: 0.4.0 flutter: plugin: @@ -21,7 +21,8 @@ dev_dependencies: mockito: ^3.0.0 flutter_test: sdk: flutter + e2e: ^0.2.0 environment: sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.5.0 <2.0.0" + flutter: ">=1.6.7 <2.0.0" diff --git a/packages/quick_actions/test/quick_actions_test.dart b/packages/quick_actions/test/quick_actions_test.dart index 98a412e3e06d..ffb6de1024fd 100644 --- a/packages/quick_actions/test/quick_actions_test.dart +++ b/packages/quick_actions/test/quick_actions_test.dart @@ -1,6 +1,8 @@ // Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:quick_actions/quick_actions.dart'; @@ -16,7 +18,7 @@ void main() { quickActions.channel.setMockMethodCallHandler( (MethodCall methodCall) async { log.add(methodCall); - return null; + return 'non empty response'; }, ); }); @@ -59,8 +61,9 @@ void main() { log.clear(); }); - test('runLaunchAction', () { - quickActions.runLaunchAction(null); + test('initialize', () async { + final Completer quickActionsHandler = Completer(); + quickActions.initialize((_) => quickActionsHandler.complete(true)); expect( log, [ @@ -68,5 +71,20 @@ void main() { ], ); log.clear(); + + expect(quickActionsHandler.future, completion(isTrue)); + }); + + test('Shortcut item can be constructed', () { + const String type = 'type'; + const String localizedTitle = 'title'; + const String icon = 'foo'; + + const ShortcutItem item = + ShortcutItem(type: type, localizedTitle: localizedTitle, icon: icon); + + expect(item.type, type); + expect(item.localizedTitle, localizedTitle); + expect(item.icon, icon); }); } diff --git a/packages/sensors/CHANGELOG.md b/packages/sensors/CHANGELOG.md index ea488586cb87..aecf8d4c83ec 100644 --- a/packages/sensors/CHANGELOG.md +++ b/packages/sensors/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.1+3 + +* Improve documentation and add unit test coverage. + ## 0.4.1+2 * Remove AndroidX warnings. diff --git a/packages/sensors/README.md b/packages/sensors/README.md index fdd450217487..4bb9f4f254e1 100644 --- a/packages/sensors/README.md +++ b/packages/sensors/README.md @@ -5,7 +5,22 @@ A Flutter plugin to access the accelerometer and gyroscope sensors. ## Usage -To use this plugin, add `sensors` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +To use this plugin, add `sensors` as a [dependency in your pubspec.yaml +file](https://flutter.io/platform-plugins/). + +This will expose three classes of sensor events, through three different +streams. + +- `AccelerometerEvent`s describe the velocity of the device, including the + effects of gravity. Put simply, you can use accelerometer readings to tell if + the device is moving in a particular direction. +- `UserAccelerometerEvent`s also describe the velocity of the device, but don't + include gravity. They can also be thought of as just the user's affect on the + device. +- `GyroscopeEvent`s describe the rotation of the device. + +Each of these is exposed through a `BroadcastStream`: `accelerometerEvents`, +`userAccelerometerEvents`, and `gyroscopeEvents`, respectively. ### Example @@ -14,10 +29,21 @@ To use this plugin, add `sensors` as a [dependency in your pubspec.yaml file](ht import 'package:sensors/sensors.dart'; accelerometerEvents.listen((AccelerometerEvent event) { - // Do something with the event. + print(event); }); +// [AccelerometerEvent (x: 0.0, y: 9.8, z: 0.0)] + +userAccelerometerEvents.listen((AccelerometerEvent event) { + print(event); +}); +// [UserAccelerometerEvent (x: 0.0, y: 0.0, z: 0.0)] gyroscopeEvents.listen((GyroscopeEvent event) { - // Do something with the event. + print(event); }); -``` \ No newline at end of file +// [GyroscopeEvent (x: 0.0, y: 0.0, z: 0.0)] + +``` + +Also see the `example` subdirectory for an example application that uses the +sensor data. diff --git a/packages/sensors/example/lib/main.dart b/packages/sensors/example/lib/main.dart index e574a64f5f38..575e0493742f 100644 --- a/packages/sensors/example/lib/main.dart +++ b/packages/sensors/example/lib/main.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: public_member_api_docs + import 'dart:async'; import 'package:flutter/material.dart'; import 'package:sensors/sensors.dart'; diff --git a/packages/sensors/example/lib/snake.dart b/packages/sensors/example/lib/snake.dart index b870791618e9..2b7cad4cf2e0 100644 --- a/packages/sensors/example/lib/snake.dart +++ b/packages/sensors/example/lib/snake.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: public_member_api_docs + import 'dart:async'; import 'dart:math' as math; diff --git a/packages/sensors/example/test_driver/test/sensors_e2e_test.dart b/packages/sensors/example/test_driver/test/sensors_e2e_test.dart index ff6e9ce74ad9..f3aa9e218d82 100644 --- a/packages/sensors/example/test_driver/test/sensors_e2e_test.dart +++ b/packages/sensors/example/test_driver/test/sensors_e2e_test.dart @@ -10,6 +10,6 @@ Future main() async { final FlutterDriver driver = await FlutterDriver.connect(); final String result = await driver.requestData(null, timeout: const Duration(minutes: 1)); - driver.close(); + await driver.close(); exit(result == 'pass' ? 0 : 1); } diff --git a/packages/sensors/lib/sensors.dart b/packages/sensors/lib/sensors.dart index 7b41a4959f94..0b6f1b5a6067 100644 --- a/packages/sensors/lib/sensors.dart +++ b/packages/sensors/lib/sensors.dart @@ -14,48 +14,95 @@ const EventChannel _userAccelerometerEventChannel = const EventChannel _gyroscopeEventChannel = EventChannel('plugins.flutter.io/sensors/gyroscope'); +/// Discrete reading from an accelerometer. Accelerometers measure the velocity +/// of the device. Note that these readings include the effects of gravity. Put +/// simply, you can use accelerometer readings to tell if the device is moving in +/// a particular direction. class AccelerometerEvent { + /// Contructs an instance with the given [x], [y], and [z] values. AccelerometerEvent(this.x, this.y, this.z); /// Acceleration force along the x axis (including gravity) measured in m/s^2. + /// + /// When the device is held upright facing the user, positive values mean the + /// device is moving to the right and negative mean it is moving to the left. final double x; /// Acceleration force along the y axis (including gravity) measured in m/s^2. + /// + /// When the device is held upright facing the user, positive values mean the + /// device is moving towards the sky and negative mean it is moving towards + /// the ground. final double y; /// Acceleration force along the z axis (including gravity) measured in m/s^2. + /// + /// This uses a right-handed coordinate system. So when the device is held + /// upright and facing the user, positive values mean the device is moving + /// towards the user and negative mean it is moving away from them. final double z; @override String toString() => '[AccelerometerEvent (x: $x, y: $y, z: $z)]'; } +/// Discrete reading from a gyroscope. Gyroscopes measure the rate or rotation of +/// the device in 3D space. class GyroscopeEvent { + /// Contructs an instance with the given [x], [y], and [z] values. GyroscopeEvent(this.x, this.y, this.z); /// Rate of rotation around the x axis measured in rad/s. + /// + /// When the device is held upright, this can also be thought of as describing + /// "pitch". The top of the device will tilt towards or away from the + /// user as this value changes. final double x; /// Rate of rotation around the y axis measured in rad/s. + /// + /// When the device is held upright, this can also be thought of as describing + /// "yaw". The lengthwise edge of the device will rotate towards or away from + /// the user as this value changes. final double y; /// Rate of rotation around the z axis measured in rad/s. + /// + /// When the device is held upright, this can also be thought of as describing + /// "roll". When this changes the face of the device should remain facing + /// forward, but the orientation will change from portrait to landscape and so + /// on. final double z; @override String toString() => '[GyroscopeEvent (x: $x, y: $y, z: $z)]'; } +/// Like [AccelerometerEvent], this is a discrete reading from an accelerometer +/// and measures the velocity of the device. However, unlike +/// [AccelerometerEvent], this event does not include the effects of gravity. class UserAccelerometerEvent { + /// Contructs an instance with the given [x], [y], and [z] values. UserAccelerometerEvent(this.x, this.y, this.z); /// Acceleration force along the x axis (excluding gravity) measured in m/s^2. + /// + /// When the device is held upright facing the user, positive values mean the + /// device is moving to the right and negative mean it is moving to the left. final double x; /// Acceleration force along the y axis (excluding gravity) measured in m/s^2. + /// + /// When the device is held upright facing the user, positive values mean the + /// device is moving towards the sky and negative mean it is moving towards + /// the ground. final double y; /// Acceleration force along the z axis (excluding gravity) measured in m/s^2. + /// + /// This uses a right-handed coordinate system. So when the device is held + /// upright and facing the user, positive values mean the device is moving + /// towards the user and negative mean it is moving away from them. final double z; @override diff --git a/packages/sensors/pubspec.yaml b/packages/sensors/pubspec.yaml index 38e698299b9e..b750d93865c6 100644 --- a/packages/sensors/pubspec.yaml +++ b/packages/sensors/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for accessing the Android and iOS accelerometer and gyroscope sensors. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/sensors -version: 0.4.1+2 +version: 0.4.1+3 flutter: plugin: @@ -20,6 +20,7 @@ dev_dependencies: flutter_test: sdk: flutter e2e: ^0.2.0 + mockito: ^4.1.1 environment: sdk: ">=2.0.0-dev.28.0 <3.0.0" diff --git a/packages/sensors/test/sensors_test.dart b/packages/sensors/test/sensors_test.dart index 603f805386fd..1485d589ad72 100644 --- a/packages/sensors/test/sensors_test.dart +++ b/packages/sensors/test/sensors_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/services.dart'; @@ -16,44 +15,67 @@ void main() { test('$accelerometerEvents are streamed', () async { const String channelName = 'plugins.flutter.io/sensors/accelerometer'; const List sensorData = [1.0, 2.0, 3.0]; + _initializeFakeSensorChannel(channelName, sensorData); - const StandardMethodCodec standardMethod = StandardMethodCodec(); - - void emitEvent(ByteData event) { - // TODO(hterkelsen): Remove this when defaultBinaryMessages is in stable. - // https://github.com/flutter/flutter/issues/33446 - // ignore: deprecated_member_use - BinaryMessages.handlePlatformMessage( - channelName, - event, - (ByteData reply) {}, - ); - } + final AccelerometerEvent event = await accelerometerEvents.first; + + expect(event.x, sensorData[0]); + expect(event.y, sensorData[1]); + expect(event.z, sensorData[2]); + }); + + test('$gyroscopeEvents are streamed', () async { + const String channelName = 'plugins.flutter.io/sensors/gyroscope'; + const List sensorData = [3.0, 4.0, 5.0]; + _initializeFakeSensorChannel(channelName, sensorData); + + final GyroscopeEvent event = await gyroscopeEvents.first; + + expect(event.x, sensorData[0]); + expect(event.y, sensorData[1]); + expect(event.z, sensorData[2]); + }); + + test('$userAccelerometerEvents are streamed', () async { + const String channelName = 'plugins.flutter.io/sensors/user_accel'; + const List sensorData = [6.0, 7.0, 8.0]; + _initializeFakeSensorChannel(channelName, sensorData); - bool isCanceled = false; + final UserAccelerometerEvent event = await userAccelerometerEvents.first; + + expect(event.x, sensorData[0]); + expect(event.y, sensorData[1]); + expect(event.z, sensorData[2]); + }); +} + +void _initializeFakeSensorChannel(String channelName, List sensorData) { + const StandardMethodCodec standardMethod = StandardMethodCodec(); + + void _emitEvent(ByteData event) { // TODO(hterkelsen): Remove this when defaultBinaryMessages is in stable. // https://github.com/flutter/flutter/issues/33446 // ignore: deprecated_member_use - BinaryMessages.setMockMessageHandler(channelName, (ByteData message) async { - final MethodCall methodCall = standardMethod.decodeMethodCall(message); - if (methodCall.method == 'listen') { - emitEvent(standardMethod.encodeSuccessEnvelope(sensorData)); - emitEvent(null); - return standardMethod.encodeSuccessEnvelope(null); - } else if (methodCall.method == 'cancel') { - isCanceled = true; - return standardMethod.encodeSuccessEnvelope(null); - } else { - fail('Expected listen or cancel'); - } - }); - - final AccelerometerEvent event = await accelerometerEvents.first; - expect(event.x, 1.0); - expect(event.y, 2.0); - expect(event.z, 3.0); + BinaryMessages.handlePlatformMessage( + channelName, + event, + (ByteData reply) {}, + ); + } - await Future.delayed(Duration.zero); - expect(isCanceled, isTrue); + // TODO(hterkelsen): Remove this when defaultBinaryMessages is in stable. + // https://github.com/flutter/flutter/issues/33446 + // ignore: deprecated_member_use + BinaryMessages.setMockMessageHandler(channelName, (ByteData message) async { + final MethodCall methodCall = standardMethod.decodeMethodCall(message); + if (methodCall.method == 'listen') { + _emitEvent(standardMethod.encodeSuccessEnvelope(sensorData)); + _emitEvent(null); + return standardMethod.encodeSuccessEnvelope(null); + } else if (methodCall.method == 'cancel') { + return standardMethod.encodeSuccessEnvelope(null); + } else { + fail('Expected listen or cancel'); + } }); } diff --git a/packages/share/CHANGELOG.md b/packages/share/CHANGELOG.md index 877cafefb2b9..f10814a7fb0a 100644 --- a/packages/share/CHANGELOG.md +++ b/packages/share/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.3+3 + +* README update. + ## 0.6.3+2 * Remove AndroidX warnings. diff --git a/packages/share/README.md b/packages/share/README.md index 4536c5818b7d..3a3aa06d9799 100644 --- a/packages/share/README.md +++ b/packages/share/README.md @@ -9,16 +9,26 @@ Wraps the ACTION_SEND Intent on Android and UIActivityViewController on iOS. ## Usage + To use this plugin, add `share` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). ## Example -Import the library via +Import the library. + ``` dart import 'package:share/share.dart'; ``` -Then invoke the static `share` method anywhere in your Dart code +Then invoke the static `share` method anywhere in your Dart code. + ``` dart Share.share('check out my website https://example.com'); ``` + +The `share` method also takes an optional `subject` that will be used when +sharing to email. + +``` dart +Share.share('check out my website https://example.com', subject: 'Look what I made!'); +``` diff --git a/packages/share/analysis_options.yaml b/packages/share/analysis_options.yaml new file mode 100644 index 000000000000..22c4e1041011 --- /dev/null +++ b/packages/share/analysis_options.yaml @@ -0,0 +1,10 @@ +# This is a temporary file to allow us to land a new set of linter rules in a +# series of manageable patches instead of one gigantic PR. It disables some of +# the new lints that are already failing on this plugin, for this plugin. It +# should be deleted and the failing lints addressed as soon as possible. + +include: ../../analysis_options.yaml + +analyzer: + errors: + unawaited_futures: ignore diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index 1eb04e0f3c64..b68195cd3507 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: public_member_api_docs + import 'package:flutter/material.dart'; import 'package:share/share.dart'; diff --git a/packages/share/pubspec.yaml b/packages/share/pubspec.yaml index de376e8fb28b..0748bd2d9d8a 100644 --- a/packages/share/pubspec.yaml +++ b/packages/share/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for sharing content via the platform share UI, using the ACTION_SEND intent on Android and UIActivityViewController on iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/share -version: 0.6.3+2 +version: 0.6.3+3 flutter: plugin: diff --git a/packages/shared_preferences/analysis_options.yaml b/packages/shared_preferences/analysis_options.yaml new file mode 100644 index 000000000000..35344bfb898c --- /dev/null +++ b/packages/shared_preferences/analysis_options.yaml @@ -0,0 +1,11 @@ +# This is a temporary file to allow us to land a new set of linter rules in a +# series of manageable patches instead of one gigantic PR. It disables some of +# the new lints that are already failing on this plugin, for this plugin. It +# should be deleted and the failing lints addressed as soon as possible. + +include: ../../analysis_options.yaml + +analyzer: + errors: + curly_braces_in_flow_control_structures: ignore + unawaited_futures: ignore diff --git a/packages/shared_preferences/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/shared_preferences/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/shared_preferences/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/shared_preferences/example/shared_preferences_example.iml b/packages/shared_preferences/example/shared_preferences_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/shared_preferences/example/shared_preferences_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md similarity index 87% rename from packages/shared_preferences/CHANGELOG.md rename to packages/shared_preferences/shared_preferences/CHANGELOG.md index 0c6ac0088bfe..d947475f349a 100644 --- a/packages/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -2,6 +2,24 @@ * Possibility to specify the file under which the preferences are stored on Android. +## 0.5.4+7 + +* Restructure the project for Web support. + +## 0.5.4+6 + +* Add missing documentation and a lint to prevent further undocumented APIs. + +## 0.5.4+5 + +* Update and migrate iOS example project by removing flutter_assets, change + "English" to "en", remove extraneous xcconfigs and framework outputs, + update to Xcode 11 build settings, and remove ARCHS. + +## 0.5.4+4 + +* `setMockInitialValues` needs to handle non-prefixed keys since that's an implementation detail. + ## 0.5.4+3 * Android: Suppress casting warnings. diff --git a/packages/shared_preferences/LICENSE b/packages/shared_preferences/shared_preferences/LICENSE similarity index 100% rename from packages/shared_preferences/LICENSE rename to packages/shared_preferences/shared_preferences/LICENSE diff --git a/packages/shared_preferences/README.md b/packages/shared_preferences/shared_preferences/README.md similarity index 83% rename from packages/shared_preferences/README.md rename to packages/shared_preferences/shared_preferences/README.md index 136c6de3dd99..dfd449337293 100644 --- a/packages/shared_preferences/README.md +++ b/packages/shared_preferences/shared_preferences/README.md @@ -42,11 +42,5 @@ _incrementCounter() async { You can populate `SharedPreferences` with initial values in your tests by running this code: ```dart -const MethodChannel('plugins.flutter.io/shared_preferences') - .setMockMethodCallHandler((MethodCall methodCall) async { - if (methodCall.method == 'getAll') { - return {}; // set initial values here if desired - } - return null; - }); +SharedPrefernces.setMockInitialValues (Map values); ``` diff --git a/packages/shared_preferences/android/build.gradle b/packages/shared_preferences/shared_preferences/android/build.gradle similarity index 100% rename from packages/shared_preferences/android/build.gradle rename to packages/shared_preferences/shared_preferences/android/build.gradle diff --git a/packages/shared_preferences/android/gradle.properties b/packages/shared_preferences/shared_preferences/android/gradle.properties similarity index 100% rename from packages/shared_preferences/android/gradle.properties rename to packages/shared_preferences/shared_preferences/android/gradle.properties diff --git a/packages/shared_preferences/android/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/shared_preferences/android/gradle/wrapper/gradle-wrapper.properties rename to packages/shared_preferences/shared_preferences/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/shared_preferences/android/settings.gradle b/packages/shared_preferences/shared_preferences/android/settings.gradle similarity index 100% rename from packages/shared_preferences/android/settings.gradle rename to packages/shared_preferences/shared_preferences/android/settings.gradle diff --git a/packages/shared_preferences/android/src/main/AndroidManifest.xml b/packages/shared_preferences/shared_preferences/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/shared_preferences/android/src/main/AndroidManifest.xml rename to packages/shared_preferences/shared_preferences/android/src/main/AndroidManifest.xml diff --git a/packages/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java similarity index 100% rename from packages/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java rename to packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java diff --git a/packages/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java similarity index 100% rename from packages/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java rename to packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java diff --git a/packages/shared_preferences/example/README.md b/packages/shared_preferences/shared_preferences/example/README.md similarity index 100% rename from packages/shared_preferences/example/README.md rename to packages/shared_preferences/shared_preferences/example/README.md diff --git a/packages/shared_preferences/example/android/app/build.gradle b/packages/shared_preferences/shared_preferences/example/android/app/build.gradle similarity index 100% rename from packages/shared_preferences/example/android/app/build.gradle rename to packages/shared_preferences/shared_preferences/example/android/app/build.gradle diff --git a/packages/shared_preferences/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/shared_preferences/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/shared_preferences/shared_preferences/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/shared_preferences/example/android/app/src/main/AndroidManifest.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/main/AndroidManifest.xml similarity index 100% rename from packages/shared_preferences/example/android/app/src/main/AndroidManifest.xml rename to packages/shared_preferences/shared_preferences/example/android/app/src/main/AndroidManifest.xml diff --git a/packages/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1Activity.java b/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1Activity.java similarity index 100% rename from packages/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1Activity.java rename to packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1Activity.java diff --git a/packages/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1ActivityTest.java b/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1ActivityTest.java similarity index 100% rename from packages/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1ActivityTest.java rename to packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1ActivityTest.java diff --git a/packages/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivity.java b/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivity.java similarity index 100% rename from packages/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivity.java rename to packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivity.java diff --git a/packages/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivityTest.java b/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivityTest.java similarity index 100% rename from packages/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivityTest.java rename to packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivityTest.java diff --git a/packages/shared_preferences/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/shared_preferences/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/shared_preferences/shared_preferences/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/shared_preferences/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/shared_preferences/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/shared_preferences/shared_preferences/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/shared_preferences/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/shared_preferences/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/shared_preferences/shared_preferences/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/shared_preferences/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/shared_preferences/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/shared_preferences/shared_preferences/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/shared_preferences/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/shared_preferences/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/shared_preferences/shared_preferences/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/shared_preferences/example/android/build.gradle b/packages/shared_preferences/shared_preferences/example/android/build.gradle similarity index 100% rename from packages/shared_preferences/example/android/build.gradle rename to packages/shared_preferences/shared_preferences/example/android/build.gradle diff --git a/packages/shared_preferences/example/android/gradle.properties b/packages/shared_preferences/shared_preferences/example/android/gradle.properties similarity index 100% rename from packages/shared_preferences/example/android/gradle.properties rename to packages/shared_preferences/shared_preferences/example/android/gradle.properties diff --git a/packages/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/shared_preferences/example/android/settings.gradle b/packages/shared_preferences/shared_preferences/example/android/settings.gradle similarity index 100% rename from packages/shared_preferences/example/android/settings.gradle rename to packages/shared_preferences/shared_preferences/example/android/settings.gradle diff --git a/packages/shared_preferences/example/ios/Flutter/AppFrameworkInfo.plist b/packages/shared_preferences/shared_preferences/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from packages/shared_preferences/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/shared_preferences/shared_preferences/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/packages/shared_preferences/example/ios/Flutter/Debug.xcconfig b/packages/shared_preferences/shared_preferences/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/shared_preferences/example/ios/Flutter/Debug.xcconfig rename to packages/shared_preferences/shared_preferences/example/ios/Flutter/Debug.xcconfig diff --git a/packages/shared_preferences/example/ios/Flutter/Release.xcconfig b/packages/shared_preferences/shared_preferences/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/shared_preferences/example/ios/Flutter/Release.xcconfig rename to packages/shared_preferences/shared_preferences/example/ios/Flutter/Release.xcconfig diff --git a/packages/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj similarity index 91% rename from packages/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj rename to packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj index 6b6940129013..2adc7021c6bf 100644 --- a/packages/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 2D92224B1EC342E7007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D92224A1EC342E7007564B0 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; @@ -15,8 +14,6 @@ 66F8BCECCEFF62F4071D2DFC /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 12E03CD14DABAA3AD3923183 /* libPods-Runner.a */; }; 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 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 */; }; @@ -40,8 +37,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 081A3238A89B77A99B096D83 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 12E03CD14DABAA3AD3923183 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 2D9222491EC342E7007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 2D92224A1EC342E7007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; @@ -49,6 +46,7 @@ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 942E815CEF30E101E045B849 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; @@ -77,6 +75,8 @@ 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { isa = PBXGroup; children = ( + 942E815CEF30E101E045B849 /* Pods-Runner.debug.xcconfig */, + 081A3238A89B77A99B096D83 /* Pods-Runner.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -86,7 +86,6 @@ children = ( 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -160,7 +159,6 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */, - 532EA9D341340B1DCD08293D /* [CP] Copy Pods Resources */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, ); buildRules = ( @@ -178,7 +176,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0830; + LastUpgradeCheck = 1100; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -188,7 +186,7 @@ }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -210,10 +208,7 @@ buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); @@ -236,37 +231,19 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; }; - 532EA9D341340B1DCD08293D /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/.symlinks/flutter/ios/Flutter.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { @@ -341,19 +318,28 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -389,19 +375,28 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -430,7 +425,6 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { - ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -452,7 +446,6 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/packages/video_player/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from packages/video_player/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/packages/shared_preferences/example/ios/Runner/AppDelegate.h b/packages/shared_preferences/shared_preferences/example/ios/Runner/AppDelegate.h similarity index 100% rename from packages/shared_preferences/example/ios/Runner/AppDelegate.h rename to packages/shared_preferences/shared_preferences/example/ios/Runner/AppDelegate.h diff --git a/packages/shared_preferences/example/ios/Runner/AppDelegate.m b/packages/shared_preferences/shared_preferences/example/ios/Runner/AppDelegate.m similarity index 100% rename from packages/shared_preferences/example/ios/Runner/AppDelegate.m rename to packages/shared_preferences/shared_preferences/example/ios/Runner/AppDelegate.m diff --git a/packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/shared_preferences/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/shared_preferences/shared_preferences/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/shared_preferences/example/ios/Runner/Base.lproj/Main.storyboard b/packages/shared_preferences/shared_preferences/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/shared_preferences/example/ios/Runner/Info.plist b/packages/shared_preferences/shared_preferences/example/ios/Runner/Info.plist similarity index 100% rename from packages/shared_preferences/example/ios/Runner/Info.plist rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Info.plist diff --git a/packages/shared_preferences/example/ios/Runner/main.m b/packages/shared_preferences/shared_preferences/example/ios/Runner/main.m similarity index 100% rename from packages/shared_preferences/example/ios/Runner/main.m rename to packages/shared_preferences/shared_preferences/example/ios/Runner/main.m diff --git a/packages/shared_preferences/example/lib/main.dart b/packages/shared_preferences/shared_preferences/example/lib/main.dart similarity index 98% rename from packages/shared_preferences/example/lib/main.dart rename to packages/shared_preferences/shared_preferences/example/lib/main.dart index 0d93675bc84d..5c7f0b0fda12 100644 --- a/packages/shared_preferences/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences/example/lib/main.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: public_member_api_docs + import 'dart:async'; import 'package:flutter/material.dart'; diff --git a/packages/shared_preferences/example/pubspec.yaml b/packages/shared_preferences/shared_preferences/example/pubspec.yaml similarity index 100% rename from packages/shared_preferences/example/pubspec.yaml rename to packages/shared_preferences/shared_preferences/example/pubspec.yaml diff --git a/packages/shared_preferences/example/test_driver/shared_preferences_e2e.dart b/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart similarity index 100% rename from packages/shared_preferences/example/test_driver/shared_preferences_e2e.dart rename to packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart diff --git a/packages/path_provider/example/test_driver/path_provider_test.dart b/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e_test.dart similarity index 72% rename from packages/path_provider/example/test_driver/path_provider_test.dart rename to packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e_test.dart index b0d3305cd652..ff6e9ce74ad9 100644 --- a/packages/path_provider/example/test_driver/path_provider_test.dart +++ b/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e_test.dart @@ -3,11 +3,13 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; - +import 'dart:io'; import 'package:flutter_driver/flutter_driver.dart'; Future main() async { final FlutterDriver driver = await FlutterDriver.connect(); - await driver.requestData(null, timeout: const Duration(minutes: 1)); + final String result = + await driver.requestData(null, timeout: const Duration(minutes: 1)); driver.close(); + exit(result == 'pass' ? 0 : 1); } diff --git a/packages/shared_preferences/ios/Assets/.gitkeep b/packages/shared_preferences/shared_preferences/ios/Assets/.gitkeep similarity index 100% rename from packages/shared_preferences/ios/Assets/.gitkeep rename to packages/shared_preferences/shared_preferences/ios/Assets/.gitkeep diff --git a/packages/shared_preferences/ios/Classes/SharedPreferencesPlugin.h b/packages/shared_preferences/shared_preferences/ios/Classes/SharedPreferencesPlugin.h similarity index 100% rename from packages/shared_preferences/ios/Classes/SharedPreferencesPlugin.h rename to packages/shared_preferences/shared_preferences/ios/Classes/SharedPreferencesPlugin.h diff --git a/packages/shared_preferences/ios/Classes/SharedPreferencesPlugin.m b/packages/shared_preferences/shared_preferences/ios/Classes/SharedPreferencesPlugin.m similarity index 100% rename from packages/shared_preferences/ios/Classes/SharedPreferencesPlugin.m rename to packages/shared_preferences/shared_preferences/ios/Classes/SharedPreferencesPlugin.m diff --git a/packages/shared_preferences/ios/shared_preferences.podspec b/packages/shared_preferences/shared_preferences/ios/shared_preferences.podspec similarity index 100% rename from packages/shared_preferences/ios/shared_preferences.podspec rename to packages/shared_preferences/shared_preferences/ios/shared_preferences.podspec diff --git a/packages/shared_preferences/lib/shared_preferences.dart b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart similarity index 94% rename from packages/shared_preferences/lib/shared_preferences.dart rename to packages/shared_preferences/shared_preferences/lib/shared_preferences.dart index 05f5d7f2bb5d..9bc773e9782a 100644 --- a/packages/shared_preferences/lib/shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart @@ -37,8 +37,7 @@ class SharedPreferences { /// For Android, see https://developer.android.com/training/data-storage/shared-preferences.html for more details on the platform implementation. static Future getInstance( {String filename = "FlutterSharedPreferences"}) async { - assert(filename != null); - if (filename == null) throw ArgumentError.notNull("filename"); + ArgumentError.checkNotNull(filename); try { return await _openedInstances.putIfAbsent(filename, () async { final Map preferencesMap = @@ -207,15 +206,20 @@ class SharedPreferences { /// Initializes the shared preferences with mock values for testing. @visibleForTesting - void setMockInitialValues(Map values) { - throw UnimplementedError("Mocking function seems unfinished"); - /*_kChannel.setMockMethodCallHandler((MethodCall methodCall) async { + static void setMockInitialValues(Map values) { + final Map newValues = + values.map((String key, dynamic value) { + String newKey = key; + if (!key.startsWith(_prefix)) { + newKey = '$_prefix$key'; + } + return MapEntry(newKey, value); + }); + _kChannel.setMockMethodCallHandler((MethodCall methodCall) async { if (methodCall.method == 'getAll') { - return values; + return newValues; } return null; }); - //_completer = null; - */ } } diff --git a/packages/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml similarity index 94% rename from packages/shared_preferences/pubspec.yaml rename to packages/shared_preferences/shared_preferences/pubspec.yaml index 988929b14e9a..416416fe99c9 100644 --- a/packages/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. author: Flutter Team -homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences +homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences version: 0.6.0 flutter: diff --git a/packages/shared_preferences/test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart similarity index 95% rename from packages/shared_preferences/test/shared_preferences_test.dart rename to packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart index e58f2ff8a56f..2494ae873b08 100755 --- a/packages/shared_preferences/test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart @@ -213,4 +213,13 @@ void main() { expect(preferences.getStringList('myList'), []); }); }); + + test('calling mock initial values with non-prefixed keys succeeds', () async { + SharedPreferences.setMockInitialValues({ + 'test': 'foo', + }); + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final String value = prefs.getString('test'); + expect(value, 'foo'); + }); } diff --git a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..5797967fe365 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md @@ -0,0 +1,4 @@ +## 1.0.0 + +* Initial release. Contains the interface and an implementation based on + method channels. diff --git a/packages/shared_preferences/shared_preferences_platform_interface/LICENSE b/packages/shared_preferences/shared_preferences_platform_interface/LICENSE new file mode 100644 index 000000000000..000b4618d2bd --- /dev/null +++ b/packages/shared_preferences/shared_preferences_platform_interface/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/shared_preferences/shared_preferences_platform_interface/README.md b/packages/shared_preferences/shared_preferences_platform_interface/README.md new file mode 100644 index 000000000000..301ba68ea361 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_platform_interface/README.md @@ -0,0 +1,25 @@ +# shared_preferences_platform_interface + +A common platform interface for the [`shared_preferences`][1] plugin. + +This interface allows platform-specific implementations of the `shared_preferences` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `shared_preferences`, extend +[`SharedPreferencesPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`SharedPreferencesLoader` by calling the `SharedPreferencesPlatform.loader` setter. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../shared_preferences +[2]: lib/shared_preferences_platform_interface.dart diff --git a/packages/shared_preferences/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart b/packages/shared_preferences/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart new file mode 100644 index 000000000000..66009a5caf14 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart @@ -0,0 +1,56 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; + +import 'shared_preferences_platform_interface.dart'; + +const MethodChannel _kChannel = + MethodChannel('plugins.flutter.io/shared_preferences'); + +/// Wraps NSUserDefaults (on iOS) and SharedPreferences (on Android), providing +/// a persistent store for simple data. +/// +/// Data is persisted to disk asynchronously. +class MethodChannelSharedPreferencesStore + extends SharedPreferencesStorePlatform { + @override + Future remove(String key) { + return _invokeBoolMethod('remove', { + 'key': key, + }); + } + + @override + Future setValue(String valueType, String key, Object value) { + return _invokeBoolMethod('set$valueType', { + 'key': key, + 'value': value, + }); + } + + Future _invokeBoolMethod(String method, Map params) { + return _kChannel + .invokeMethod(method, params) + // TODO(yjbanov): I copied this from the original + // shared_preferences.dart implementation, but I + // actually do not know why it's necessary to pipe the + // result through an identity function. + // + // Source: https://github.com/flutter/plugins/blob/3a87296a40a2624d200917d58f036baa9fb18df8/packages/shared_preferences/lib/shared_preferences.dart#L134 + .then((dynamic result) => result); + } + + @override + Future clear() { + return _kChannel.invokeMethod('clear'); + } + + @override + Future> getAll() { + return _kChannel.invokeMapMethod('getAll'); + } +} diff --git a/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_platform_interface.dart b/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_platform_interface.dart new file mode 100644 index 000000000000..ff42f6357e74 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_platform_interface.dart @@ -0,0 +1,108 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:meta/meta.dart'; + +import 'method_channel_shared_preferences.dart'; + +/// The interface that implementations of shared_preferences must implement. +/// +/// Platform implementations should extend this class rather than implement it as `shared_preferences` +/// does not consider newly added methods to be breaking changes. Extending this class +/// (using `extends`) ensures that the subclass will get the default implementation, while +/// platform implementations that `implements` this interface will be broken by newly added +/// [SharedPreferencesStorePlatform] methods. +abstract class SharedPreferencesStorePlatform { + /// The default instance of [SharedPreferencesStorePlatform] to use. + /// + /// Defaults to [MethodChannelSharedPreferencesStore]. + static SharedPreferencesStorePlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [SharedPreferencesStorePlatform] when they register themselves. + static set instance(SharedPreferencesStorePlatform value) { + try { + instance._verifyProvidesDefaultImplementations(); + _instance = value; + } on NoSuchMethodError catch (_) {} + } + + static SharedPreferencesStorePlatform _instance = + MethodChannelSharedPreferencesStore(); + + /// Only mock implementations should set this to true. + /// + /// Mockito mocks are implementing this class with `implements` which is forbidden for anything + /// other than mocks (see class docs). This property provides a backdoor for mockito mocks to + /// skip the verification that the class isn't implemented with `implements`. + @visibleForTesting + bool get isMock => false; + + /// Removes the value associated with the [key]. + Future remove(String key); + + /// Stores the [value] associated with the [key]. + /// + /// The [valueType] must match the type of [value] as follows: + /// + /// * Value type "Bool" must be passed if the value is of type `bool`. + /// * Value type "Double" must be passed if the value is of type `double`. + /// * Value type "Int" must be passed if the value is of type `int`. + /// * Value type "String" must be passed if the value is of type `String`. + /// * Value type "StringList" must be passed if the value is of type `List`. + Future setValue(String valueType, String key, Object value); + + /// Removes all keys and values in the store. + Future clear(); + + /// Returns all key/value pairs persisted in this store. + Future> getAll(); + + // This method makes sure that SharedPreferencesStorePlatform isn't implemented with `implements`. + // + // See class doc for more details on why implementing this class is forbidden. + // + // This private method is called by the instance setter, which fails if the class is + // implemented with `implements`. + void _verifyProvidesDefaultImplementations() {} +} + +/// Stores data in memory. +/// +/// Data does not persist across application restarts. This is useful in unit-tests. +class InMemorySharedPreferencesStore extends SharedPreferencesStorePlatform { + /// Instantiates an empty in-memory preferences store. + InMemorySharedPreferencesStore.empty() : _data = {}; + + /// Instantiates an in-memory preferences store containing a copy of [data]. + InMemorySharedPreferencesStore.withData(Map data) + : _data = Map.from(data); + + final Map _data; + + @override + Future clear() async { + _data.clear(); + return true; + } + + @override + Future> getAll() async { + return Map.from(_data); + } + + @override + Future remove(String key) async { + _data.remove(key); + return true; + } + + @override + Future setValue(String valueType, String key, Object value) async { + _data[key] = value; + return true; + } +} diff --git a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..92efc896f777 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml @@ -0,0 +1,18 @@ +name: shared_preferences_platform_interface +description: A common platform interface for the shared_preferences plugin. +author: Flutter Team +homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_platform_interface +version: 1.0.0 + +dependencies: + meta: ^1.0.4 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +environment: + sdk: ">=2.0.0-dev.28.0 <3.0.0" + flutter: ">=1.6.7 <2.0.0" diff --git a/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart new file mode 100644 index 000000000000..4cc79b058675 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart @@ -0,0 +1,112 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; +import 'package:shared_preferences_platform_interface/method_channel_shared_preferences.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group(MethodChannelSharedPreferencesStore, () { + const MethodChannel channel = MethodChannel( + 'plugins.flutter.io/shared_preferences', + ); + + const Map kTestValues = { + 'flutter.String': 'hello world', + 'flutter.Bool': true, + 'flutter.Int': 42, + 'flutter.Double': 3.14159, + 'flutter.StringList': ['foo', 'bar'], + }; + + InMemorySharedPreferencesStore testData; + + final List log = []; + MethodChannelSharedPreferencesStore store; + + setUp(() async { + testData = InMemorySharedPreferencesStore.empty(); + + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + if (methodCall.method == 'getAll') { + return await testData.getAll(); + } + if (methodCall.method == 'remove') { + final String key = methodCall.arguments['key']; + return await testData.remove(key); + } + if (methodCall.method == 'clear') { + return await testData.clear(); + } + final RegExp setterRegExp = RegExp(r'set(.*)'); + final Match match = setterRegExp.matchAsPrefix(methodCall.method); + if (match.groupCount == 1) { + final String valueType = match.group(1); + final String key = methodCall.arguments['key']; + final Object value = methodCall.arguments['value']; + return await testData.setValue(valueType, key, value); + } + fail('Unexpected method call: ${methodCall.method}'); + }); + store = MethodChannelSharedPreferencesStore(); + log.clear(); + }); + + tearDown(() async { + await testData.clear(); + store = null; + testData = null; + }); + + test('getAll', () async { + testData = InMemorySharedPreferencesStore.withData(kTestValues); + expect(await store.getAll(), kTestValues); + expect(log.single.method, 'getAll'); + }); + + test('remove', () async { + testData = InMemorySharedPreferencesStore.withData(kTestValues); + expect(await store.remove('flutter.String'), true); + expect(await store.remove('flutter.Bool'), true); + expect(await store.remove('flutter.Int'), true); + expect(await store.remove('flutter.Double'), true); + expect(await testData.getAll(), { + 'flutter.StringList': ['foo', 'bar'], + }); + + expect(log, hasLength(4)); + for (MethodCall call in log) { + expect(call.method, 'remove'); + } + }); + + test('setValue', () async { + expect(await testData.getAll(), isEmpty); + for (String key in kTestValues.keys) { + final dynamic value = kTestValues[key]; + expect(await store.setValue(key.split('.').last, key, value), true); + } + expect(await testData.getAll(), kTestValues); + + expect(log, hasLength(5)); + expect(log[0].method, 'setString'); + expect(log[1].method, 'setBool'); + expect(log[2].method, 'setInt'); + expect(log[3].method, 'setDouble'); + expect(log[4].method, 'setStringList'); + }); + + test('clear', () async { + testData = InMemorySharedPreferencesStore.withData(kTestValues); + expect(await testData.getAll(), isNotEmpty); + expect(await store.clear(), true); + expect(await testData.getAll(), isEmpty); + expect(log.single.method, 'clear'); + }); + }); +} diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index 300d6d9342f5..1a07090992d5 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.2.7 + +* Minor unit test changes and added a lint for public DartDocs. + ## 5.2.6 * Remove AndroidX warnings. diff --git a/packages/url_launcher/url_launcher/example/lib/main.dart b/packages/url_launcher/url_launcher/example/lib/main.dart index b4a7e4275bfc..00ab6c5b047d 100644 --- a/packages/url_launcher/url_launcher/example/lib/main.dart +++ b/packages/url_launcher/url_launcher/example/lib/main.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: public_member_api_docs + import 'dart:async'; import 'package:flutter/material.dart'; diff --git a/packages/url_launcher/url_launcher/example/test_driver/url_launcher_e2e_test.dart b/packages/url_launcher/url_launcher/example/test_driver/url_launcher_e2e_test.dart index ac4ea11482e2..1bcd0d37f450 100644 --- a/packages/url_launcher/url_launcher/example/test_driver/url_launcher_e2e_test.dart +++ b/packages/url_launcher/url_launcher/example/test_driver/url_launcher_e2e_test.dart @@ -10,6 +10,6 @@ Future main() async { final FlutterDriver driver = await FlutterDriver.connect(); final String result = await driver.requestData(null, timeout: const Duration(minutes: 1)); - driver.close(); + await driver.close(); exit(result == 'pass' ? 0 : 1); } diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index 2477e118586e..e0c59134f8f2 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for launching a URL on Android and iOS. Supports web, phone, SMS, and email schemes. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher -version: 5.2.6 +version: 5.2.7 flutter: plugin: diff --git a/packages/url_launcher/url_launcher/test/url_launcher_test.dart b/packages/url_launcher/url_launcher/test/url_launcher_test.dart index e7150bebde05..2481e5a0ee63 100644 --- a/packages/url_launcher/url_launcher/test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher/test/url_launcher_test.dart @@ -2,8 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; +import 'dart:ui'; +import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; +import 'package:flutter/foundation.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; import 'package:flutter/services.dart' show PlatformException; @@ -11,166 +14,197 @@ import 'package:flutter/services.dart' show PlatformException; void main() { final MockUrlLauncher mock = MockUrlLauncher(); when(mock.isMock).thenReturn(true); - UrlLauncherPlatform.instance = mock; - test('canLaunch', () async { - await canLaunch('http://example.com/'); - expect(verify(mock.canLaunch(captureAny)).captured.single, - 'http://example.com/'); + test('closeWebView default behavior', () async { + await closeWebView(); + verify(mock.closeWebView()); }); - test('launch default behavior', () async { - await launch('http://example.com/'); - expect( - verify(mock.launch( - captureAny, - useSafariVC: captureAnyNamed('useSafariVC'), - useWebView: captureAnyNamed('useWebView'), - enableJavaScript: captureAnyNamed('enableJavaScript'), - enableDomStorage: captureAnyNamed('enableDomStorage'), - universalLinksOnly: captureAnyNamed('universalLinksOnly'), - headers: captureAnyNamed('headers'), - )).captured, - [ - 'http://example.com/', - true, - false, - false, - false, - false, - {}, - ], - ); - }); + group('canLaunch', () { + test('returns true', () async { + when(mock.canLaunch('foo')).thenAnswer((_) => Future.value(true)); - test('launch with headers', () async { - await launch( - 'http://example.com/', - headers: {'key': 'value'}, - ); - expect( - verify(mock.launch( - any, - useSafariVC: anyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: captureAnyNamed('headers'), - )).captured.single, - {'key': 'value'}, - ); - }); + final bool result = await canLaunch('foo'); - test('launch force SafariVC', () async { - await launch('http://example.com/', forceSafariVC: true); - expect( - verify(mock.launch( - any, - useSafariVC: captureAnyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured.single, - true, - ); - }); + expect(result, isTrue); + }); - test('launch universal links only', () async { - await launch('http://example.com/', - forceSafariVC: false, universalLinksOnly: true); - expect( - verify(mock.launch( - any, - useSafariVC: captureAnyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: captureAnyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured, - [false, true], - ); - }); + test('returns false', () async { + when(mock.canLaunch('foo')).thenAnswer((_) => Future.value(false)); - test('launch force WebView', () async { - await launch('http://example.com/', forceWebView: true); - expect( - verify(mock.launch( - any, - useSafariVC: anyNamed('useSafariVC'), - useWebView: captureAnyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured.single, - true, - ); - }); + final bool result = await canLaunch('foo'); - test('launch force WebView enable javascript', () async { - await launch('http://example.com/', - forceWebView: true, enableJavaScript: true); - expect( - verify(mock.launch( - any, - useSafariVC: anyNamed('useSafariVC'), - useWebView: captureAnyNamed('useWebView'), - enableJavaScript: captureAnyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured, - [true, true], - ); + expect(result, isFalse); + }); }); + group('launch', () { + test('requires a non-null urlString', () { + expect(() => launch(null), throwsAssertionError); + }); - test('launch force WebView enable DOM storage', () async { - await launch('http://example.com/', - forceWebView: true, enableDomStorage: true); - expect( - verify(mock.launch( - any, - useSafariVC: anyNamed('useSafariVC'), - useWebView: captureAnyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: captureAnyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured, - [true, true], - ); - }); + test('default behavior', () async { + await launch('http://flutter.dev/'); + expect( + verify(mock.launch( + captureAny, + useSafariVC: captureAnyNamed('useSafariVC'), + useWebView: captureAnyNamed('useWebView'), + enableJavaScript: captureAnyNamed('enableJavaScript'), + enableDomStorage: captureAnyNamed('enableDomStorage'), + universalLinksOnly: captureAnyNamed('universalLinksOnly'), + headers: captureAnyNamed('headers'), + )).captured, + [ + 'http://flutter.dev/', + true, + false, + false, + false, + false, + {}, + ], + ); + }); - test('launch force SafariVC to false', () async { - await launch('http://example.com/', forceSafariVC: false); - expect( - // ignore: missing_required_param - verify(mock.launch( - any, - useSafariVC: captureAnyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured.single, - false, - ); - }); + test('with headers', () async { + await launch( + 'http://flutter.dev/', + headers: {'key': 'value'}, + ); + expect( + verify(mock.launch( + any, + useSafariVC: anyNamed('useSafariVC'), + useWebView: anyNamed('useWebView'), + enableJavaScript: anyNamed('enableJavaScript'), + enableDomStorage: anyNamed('enableDomStorage'), + universalLinksOnly: anyNamed('universalLinksOnly'), + headers: captureAnyNamed('headers'), + )).captured.single, + {'key': 'value'}, + ); + }); - test('cannot launch a non-web in webview', () async { - expect(() async => await launch('tel:555-555-5555', forceWebView: true), - throwsA(isA())); - }); + test('force SafariVC', () async { + await launch('http://flutter.dev/', forceSafariVC: true); + expect( + verify(mock.launch( + any, + useSafariVC: captureAnyNamed('useSafariVC'), + useWebView: anyNamed('useWebView'), + enableJavaScript: anyNamed('enableJavaScript'), + enableDomStorage: anyNamed('enableDomStorage'), + universalLinksOnly: anyNamed('universalLinksOnly'), + headers: anyNamed('headers'), + )).captured.single, + true, + ); + }); - test('closeWebView default behavior', () async { - await closeWebView(); - verify(mock.closeWebView()); + test('universal links only', () async { + await launch('http://flutter.dev/', + forceSafariVC: false, universalLinksOnly: true); + expect( + verify(mock.launch( + any, + useSafariVC: captureAnyNamed('useSafariVC'), + useWebView: anyNamed('useWebView'), + enableJavaScript: anyNamed('enableJavaScript'), + enableDomStorage: anyNamed('enableDomStorage'), + universalLinksOnly: captureAnyNamed('universalLinksOnly'), + headers: anyNamed('headers'), + )).captured, + [false, true], + ); + }); + + test('force WebView', () async { + await launch('http://flutter.dev/', forceWebView: true); + expect( + verify(mock.launch( + any, + useSafariVC: anyNamed('useSafariVC'), + useWebView: captureAnyNamed('useWebView'), + enableJavaScript: anyNamed('enableJavaScript'), + enableDomStorage: anyNamed('enableDomStorage'), + universalLinksOnly: anyNamed('universalLinksOnly'), + headers: anyNamed('headers'), + )).captured.single, + true, + ); + }); + + test('force WebView enable javascript', () async { + await launch('http://flutter.dev/', + forceWebView: true, enableJavaScript: true); + expect( + verify(mock.launch( + any, + useSafariVC: anyNamed('useSafariVC'), + useWebView: captureAnyNamed('useWebView'), + enableJavaScript: captureAnyNamed('enableJavaScript'), + enableDomStorage: anyNamed('enableDomStorage'), + universalLinksOnly: anyNamed('universalLinksOnly'), + headers: anyNamed('headers'), + )).captured, + [true, true], + ); + }); + + test('force WebView enable DOM storage', () async { + await launch('http://flutter.dev/', + forceWebView: true, enableDomStorage: true); + expect( + verify(mock.launch( + any, + useSafariVC: anyNamed('useSafariVC'), + useWebView: captureAnyNamed('useWebView'), + enableJavaScript: anyNamed('enableJavaScript'), + enableDomStorage: captureAnyNamed('enableDomStorage'), + universalLinksOnly: anyNamed('universalLinksOnly'), + headers: anyNamed('headers'), + )).captured, + [true, true], + ); + }); + + test('force SafariVC to false', () async { + await launch('http://flutter.dev/', forceSafariVC: false); + expect( + // ignore: missing_required_param + verify(mock.launch( + any, + useSafariVC: captureAnyNamed('useSafariVC'), + useWebView: anyNamed('useWebView'), + enableJavaScript: anyNamed('enableJavaScript'), + enableDomStorage: anyNamed('enableDomStorage'), + universalLinksOnly: anyNamed('universalLinksOnly'), + headers: anyNamed('headers'), + )).captured.single, + false, + ); + }); + + test('cannot launch a non-web in webview', () async { + expect(() async => await launch('tel:555-555-5555', forceWebView: true), + throwsA(isA())); + }); + + test('controls system UI when changing statusBarBrightness', () async { + final TestWidgetsFlutterBinding binding = + TestWidgetsFlutterBinding.ensureInitialized(); + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + binding.renderView.automaticSystemUiAdjustment = true; + final Future launchResult = + launch('http://flutter.dev/', statusBarBrightness: Brightness.dark); + + // Should take over control of the automaticSystemUiAdjustment while it's + // pending, then restore it back to normal after the launch finishes. + expect(binding.renderView.automaticSystemUiAdjustment, isFalse); + await launchResult; + expect(binding.renderView.automaticSystemUiAdjustment, isTrue); + }); }); } diff --git a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md index 7aa8ee011524..18d373d183f2 100644 --- a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.3 + +* Minor DartDoc changes and add a lint for missing DartDocs. + ## 1.0.2 * Use package URI in test directory to import code from lib. diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart index a17aa0626126..64eeec7cae2d 100644 --- a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart +++ b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart @@ -24,17 +24,15 @@ abstract class UrlLauncherPlatform { @visibleForTesting bool get isMock => false; + static UrlLauncherPlatform _instance = MethodChannelUrlLauncher(); + /// The default instance of [UrlLauncherPlatform] to use. /// - /// Platform-specific plugins should override this with their own - /// platform-specific class that extends [UrlLauncherPlatform] when they - /// register themselves. - /// /// Defaults to [MethodChannelUrlLauncher]. - static UrlLauncherPlatform _instance = MethodChannelUrlLauncher(); - static UrlLauncherPlatform get instance => _instance; + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [UrlLauncherPlatform] when they register themselves. // TODO(amirh): Extract common platform interface logic. // https://github.com/flutter/flutter/issues/43368 static set instance(UrlLauncherPlatform instance) { diff --git a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml index 192fbe7e8066..3f7aa4832c41 100644 --- a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml +++ b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.0.2 +version: 1.0.3 dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md index e20f5fbbb636..a29a2ee1208e 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.1.0 + +- Update docs and pubspec. + # 0.0.2 - Switch to using `url_launcher_platform_interface`. diff --git a/packages/url_launcher/url_launcher_web/README.md b/packages/url_launcher/url_launcher_web/README.md index 66a586c57872..ddac7d870f98 100644 --- a/packages/url_launcher/url_launcher_web/README.md +++ b/packages/url_launcher/url_launcher_web/README.md @@ -4,22 +4,27 @@ The web implementation of [`url_launcher`][1]. ## Usage +### Import the package To use this plugin in your Flutter Web app, simply add it as a dependency in -your pubspec using a `git` dependency. This is only temporary: in the future -we hope to make this package an "endorsed" implementation of `url_launcher`, -so that it is automatically included in your Flutter Web app when you depend -on `package:url_launcher`. +your pubspec alongside the base `url_launcher` plugin. + +_(This is only temporary: in the future we hope to make this package an +"endorsed" implementation of `url_launcher`, so that it is automatically +included in your Flutter Web app when you depend on `package:url_launcher`.)_ + +This is what the above means to your `pubspec.yaml`: ```yaml +... dependencies: + ... url_launcher: ^5.1.4 - url_launcher_web: - git: - url: git://github.com/flutter/plugins.git - path: packages/url_launcher/url_launcher_web + url_launcher_web: ^0.1.0 + ... ``` +### Use the plugin Once you have the `url_launcher_web` dependency in your pubspec, you should be able to use `package:url_launcher` as normal. -[1]: ../url_launcher +[1]: ../url_launcher/url_launcher diff --git a/packages/url_launcher/url_launcher_web/pubspec.yaml b/packages/url_launcher/url_launcher_web/pubspec.yaml index e94a4d0d81c0..16c84fae366a 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_web description: Web platform implementation of url_launcher author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_web -version: 0.0.2 +version: 0.1.0 flutter: plugin: @@ -12,18 +12,17 @@ flutter: fileName: url_launcher_web.dart dependencies: + url_launcher_platform_interface: ^1.0.1 flutter: sdk: flutter flutter_web_plugins: sdk: flutter meta: ^1.1.7 - url_launcher_platform_interface: ^1.0.1 dev_dependencies: flutter_test: sdk: flutter - url_launcher: - path: ../url_launcher + url_launcher: ^5.2.5 environment: sdk: ">=2.0.0-dev.28.0 <3.0.0" diff --git a/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java deleted file mode 100644 index 5b1f55fe14d6..000000000000 --- a/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ /dev/null @@ -1,443 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.videoplayer; - -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; - -import android.content.Context; -import android.net.Uri; -import android.os.Build; -import android.util.LongSparseArray; -import android.view.Surface; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.EventListener; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.audio.AudioAttributes; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelector; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.util.Util; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.PluginRegistry; -import io.flutter.plugin.common.PluginRegistry.Registrar; -import io.flutter.view.FlutterNativeView; -import io.flutter.view.TextureRegistry; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class VideoPlayerPlugin implements MethodCallHandler { - - private static class VideoPlayer { - private static final String FORMAT_SS = "ss"; - private static final String FORMAT_DASH = "dash"; - private static final String FORMAT_HLS = "hls"; - private static final String FORMAT_OTHER = "other"; - - private SimpleExoPlayer exoPlayer; - - private Surface surface; - - private final TextureRegistry.SurfaceTextureEntry textureEntry; - - private QueuingEventSink eventSink = new QueuingEventSink(); - - private final EventChannel eventChannel; - - private boolean isInitialized = false; - - VideoPlayer( - Context context, - EventChannel eventChannel, - TextureRegistry.SurfaceTextureEntry textureEntry, - String dataSource, - Result result, - String formatHint) { - this.eventChannel = eventChannel; - this.textureEntry = textureEntry; - - TrackSelector trackSelector = new DefaultTrackSelector(); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector); - - Uri uri = Uri.parse(dataSource); - - DataSource.Factory dataSourceFactory; - if (isHTTP(uri)) { - dataSourceFactory = - new DefaultHttpDataSourceFactory( - "ExoPlayer", - null, - DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, - DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, - true); - } else { - dataSourceFactory = new DefaultDataSourceFactory(context, "ExoPlayer"); - } - - MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context); - exoPlayer.prepare(mediaSource); - - setupVideoPlayer(eventChannel, textureEntry, result); - } - - private static boolean isHTTP(Uri uri) { - if (uri == null || uri.getScheme() == null) { - return false; - } - String scheme = uri.getScheme(); - return scheme.equals("http") || scheme.equals("https"); - } - - private MediaSource buildMediaSource( - Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) { - int type; - if (formatHint == null) { - type = Util.inferContentType(uri.getLastPathSegment()); - } else { - switch (formatHint) { - case FORMAT_SS: - type = C.TYPE_SS; - break; - case FORMAT_DASH: - type = C.TYPE_DASH; - break; - case FORMAT_HLS: - type = C.TYPE_HLS; - break; - case FORMAT_OTHER: - type = C.TYPE_OTHER; - break; - default: - type = -1; - break; - } - } - switch (type) { - case C.TYPE_SS: - return new SsMediaSource.Factory( - new DefaultSsChunkSource.Factory(mediaDataSourceFactory), - new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) - .createMediaSource(uri); - case C.TYPE_DASH: - return new DashMediaSource.Factory( - new DefaultDashChunkSource.Factory(mediaDataSourceFactory), - new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) - .createMediaSource(uri); - case C.TYPE_HLS: - return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); - case C.TYPE_OTHER: - return new ExtractorMediaSource.Factory(mediaDataSourceFactory) - .setExtractorsFactory(new DefaultExtractorsFactory()) - .createMediaSource(uri); - default: - { - throw new IllegalStateException("Unsupported type: " + type); - } - } - } - - private void setupVideoPlayer( - EventChannel eventChannel, - TextureRegistry.SurfaceTextureEntry textureEntry, - Result result) { - - eventChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink sink) { - eventSink.setDelegate(sink); - } - - @Override - public void onCancel(Object o) { - eventSink.setDelegate(null); - } - }); - - surface = new Surface(textureEntry.surfaceTexture()); - exoPlayer.setVideoSurface(surface); - setAudioAttributes(exoPlayer); - - exoPlayer.addListener( - new EventListener() { - - @Override - public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { - if (playbackState == Player.STATE_BUFFERING) { - sendBufferingUpdate(); - } else if (playbackState == Player.STATE_READY) { - if (!isInitialized) { - isInitialized = true; - sendInitialized(); - } - } else if (playbackState == Player.STATE_ENDED) { - Map event = new HashMap<>(); - event.put("event", "completed"); - eventSink.success(event); - } - } - - @Override - public void onPlayerError(final ExoPlaybackException error) { - if (eventSink != null) { - eventSink.error("VideoError", "Video player had error " + error, null); - } - } - }); - - Map reply = new HashMap<>(); - reply.put("textureId", textureEntry.id()); - result.success(reply); - } - - private void sendBufferingUpdate() { - Map event = new HashMap<>(); - event.put("event", "bufferingUpdate"); - List range = Arrays.asList(0, exoPlayer.getBufferedPosition()); - // iOS supports a list of buffered ranges, so here is a list with a single range. - event.put("values", Collections.singletonList(range)); - eventSink.success(event); - } - - @SuppressWarnings("deprecation") - private static void setAudioAttributes(SimpleExoPlayer exoPlayer) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - exoPlayer.setAudioAttributes( - new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build()); - } else { - exoPlayer.setAudioStreamType(C.STREAM_TYPE_MUSIC); - } - } - - void play() { - exoPlayer.setPlayWhenReady(true); - } - - void pause() { - exoPlayer.setPlayWhenReady(false); - } - - void setLooping(boolean value) { - exoPlayer.setRepeatMode(value ? REPEAT_MODE_ALL : REPEAT_MODE_OFF); - } - - void setVolume(double value) { - float bracketedValue = (float) Math.max(0.0, Math.min(1.0, value)); - exoPlayer.setVolume(bracketedValue); - } - - void seekTo(int location) { - exoPlayer.seekTo(location); - } - - long getPosition() { - return exoPlayer.getCurrentPosition(); - } - - @SuppressWarnings("SuspiciousNameCombination") - private void sendInitialized() { - if (isInitialized) { - Map event = new HashMap<>(); - event.put("event", "initialized"); - event.put("duration", exoPlayer.getDuration()); - - if (exoPlayer.getVideoFormat() != null) { - Format videoFormat = exoPlayer.getVideoFormat(); - int width = videoFormat.width; - int height = videoFormat.height; - int rotationDegrees = videoFormat.rotationDegrees; - // Switch the width/height if video was taken in portrait mode - if (rotationDegrees == 90 || rotationDegrees == 270) { - width = exoPlayer.getVideoFormat().height; - height = exoPlayer.getVideoFormat().width; - } - event.put("width", width); - event.put("height", height); - } - eventSink.success(event); - } - } - - void dispose() { - if (isInitialized) { - exoPlayer.stop(); - } - textureEntry.release(); - eventChannel.setStreamHandler(null); - if (surface != null) { - surface.release(); - } - if (exoPlayer != null) { - exoPlayer.release(); - } - } - } - - public static void registerWith(Registrar registrar) { - final VideoPlayerPlugin plugin = new VideoPlayerPlugin(registrar); - final MethodChannel channel = - new MethodChannel(registrar.messenger(), "flutter.io/videoPlayer"); - channel.setMethodCallHandler(plugin); - registrar.addViewDestroyListener( - new PluginRegistry.ViewDestroyListener() { - @Override - public boolean onViewDestroy(FlutterNativeView view) { - plugin.onDestroy(); - return false; // We are not interested in assuming ownership of the NativeView. - } - }); - } - - private VideoPlayerPlugin(Registrar registrar) { - this.registrar = registrar; - this.videoPlayers = new LongSparseArray<>(); - } - - private final LongSparseArray videoPlayers; - - private final Registrar registrar; - - private void disposeAllPlayers() { - for (int i = 0; i < videoPlayers.size(); i++) { - videoPlayers.valueAt(i).dispose(); - } - videoPlayers.clear(); - } - - private void onDestroy() { - // The whole FlutterView is being destroyed. Here we release resources acquired for all - // instances - // of VideoPlayer. Once https://github.com/flutter/flutter/issues/19358 is resolved this may - // be replaced with just asserting that videoPlayers.isEmpty(). - // https://github.com/flutter/flutter/issues/20989 tracks this. - disposeAllPlayers(); - } - - @Override - public void onMethodCall(MethodCall call, Result result) { - TextureRegistry textures = registrar.textures(); - if (textures == null) { - result.error("no_activity", "video_player plugin requires a foreground activity", null); - return; - } - switch (call.method) { - case "init": - disposeAllPlayers(); - break; - case "create": - { - TextureRegistry.SurfaceTextureEntry handle = textures.createSurfaceTexture(); - EventChannel eventChannel = - new EventChannel( - registrar.messenger(), "flutter.io/videoPlayer/videoEvents" + handle.id()); - - VideoPlayer player; - if (call.argument("asset") != null) { - String assetLookupKey; - if (call.argument("package") != null) { - assetLookupKey = - registrar.lookupKeyForAsset(call.argument("asset"), call.argument("package")); - } else { - assetLookupKey = registrar.lookupKeyForAsset(call.argument("asset")); - } - player = - new VideoPlayer( - registrar.context(), - eventChannel, - handle, - "asset:///" + assetLookupKey, - result, - null); - videoPlayers.put(handle.id(), player); - } else { - player = - new VideoPlayer( - registrar.context(), - eventChannel, - handle, - call.argument("uri"), - result, - call.argument("formatHint")); - videoPlayers.put(handle.id(), player); - } - break; - } - default: - { - long textureId = ((Number) call.argument("textureId")).longValue(); - VideoPlayer player = videoPlayers.get(textureId); - if (player == null) { - result.error( - "Unknown textureId", - "No video player associated with texture id " + textureId, - null); - return; - } - onMethodCall(call, result, textureId, player); - break; - } - } - } - - private void onMethodCall(MethodCall call, Result result, long textureId, VideoPlayer player) { - switch (call.method) { - case "setLooping": - player.setLooping(call.argument("looping")); - result.success(null); - break; - case "setVolume": - player.setVolume(call.argument("volume")); - result.success(null); - break; - case "play": - player.play(); - result.success(null); - break; - case "pause": - player.pause(); - result.success(null); - break; - case "seekTo": - int location = ((Number) call.argument("location")).intValue(); - player.seekTo(location); - result.success(null); - break; - case "position": - result.success(player.getPosition()); - player.sendBufferingUpdate(); - break; - case "dispose": - player.dispose(); - videoPlayers.remove(textureId); - result.success(null); - break; - default: - result.notImplemented(); - break; - } - } -} diff --git a/packages/video_player/example/android.iml b/packages/video_player/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/video_player/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/video_player/example/android/app/src/main/AndroidManifest.xml b/packages/video_player/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 914e82b3c894..000000000000 --- a/packages/video_player/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - diff --git a/packages/video_player/example/android/gradle.properties b/packages/video_player/example/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/video_player/example/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/video_player/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/video_player/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/video_player/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/google_maps_flutter/example/test_driver/test_widgets.dart b/packages/video_player/example/test_driver/video_player.dart similarity index 55% rename from packages/google_maps_flutter/example/test_driver/test_widgets.dart rename to packages/video_player/example/test_driver/video_player.dart index 5656c9f5610c..cc498f41fccb 100644 --- a/packages/google_maps_flutter/example/test_driver/test_widgets.dart +++ b/packages/video_player/example/test_driver/video_player.dart @@ -2,11 +2,10 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'dart:async'; +import 'package:flutter_driver/driver_extension.dart'; +import 'package:video_player_example/main.dart' as app; -import 'package:flutter/widgets.dart'; - -Future pumpWidget(Widget widget) { - runApp(widget); - return WidgetsBinding.instance.endOfFrame; +void main() { + enableFlutterDriverExtension(); + app.main(); } diff --git a/packages/video_player/example/test_driver/video_player_test.dart b/packages/video_player/example/test_driver/video_player_test.dart new file mode 100644 index 000000000000..a72c6944cfbe --- /dev/null +++ b/packages/video_player/example/test_driver/video_player_test.dart @@ -0,0 +1,27 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:test/test.dart'; + +Future main() async { + final FlutterDriver driver = await FlutterDriver.connect(); + tearDownAll(() async { + driver.close(); + }); + + //TODO(cyanglaz): Use TabBar tabs to navigate between pages after https://github.com/flutter/flutter/issues/16991 is fixed. + //TODO(cyanglaz): Un-skip the test after https://github.com/flutter/flutter/issues/43012 is fixed + test('Push a page contains video and pop back, do not crash.', () async { + final SerializableFinder pushTab = find.byValueKey('push_tab'); + await driver.waitFor(pushTab); + await driver.tap(pushTab); + await driver.waitForAbsent(pushTab); + await driver.waitFor(find.byValueKey('home_page')); + await driver.waitUntilNoTransientCallbacks(); + final Health health = await driver.checkHealth(); + expect(health.status, HealthStatus.ok); + }, skip: 'Cirrus CI currently hangs while playing videos'); +} diff --git a/packages/video_player/test/video_player_test.dart b/packages/video_player/test/video_player_test.dart deleted file mode 100644 index 9211f0c3a87c..000000000000 --- a/packages/video_player/test/video_player_test.dart +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:video_player/video_player.dart'; -import 'package:flutter_test/flutter_test.dart'; - -class FakeController extends ValueNotifier - implements VideoPlayerController { - FakeController() : super(VideoPlayerValue(duration: null)); - - @override - Future dispose() async { - super.dispose(); - } - - @override - int textureId; - - @override - String get dataSource => ''; - @override - DataSourceType get dataSourceType => DataSourceType.file; - @override - String get package => null; - @override - Future get position async => value.position; - - @override - Future seekTo(Duration moment) async {} - @override - Future setVolume(double volume) async {} - @override - Future initialize() async {} - @override - Future pause() async {} - @override - Future play() async {} - @override - Future setLooping(bool looping) async {} - - @override - VideoFormat get formatHint => null; -} - -void main() { - testWidgets('update texture', (WidgetTester tester) async { - final FakeController controller = FakeController(); - await tester.pumpWidget(VideoPlayer(controller)); - expect(find.byType(Texture), findsNothing); - - controller.textureId = 123; - controller.value = controller.value.copyWith( - duration: const Duration(milliseconds: 100), - ); - - await tester.pump(); - expect(find.byType(Texture), findsOneWidget); - }); - - testWidgets('update controller', (WidgetTester tester) async { - final FakeController controller1 = FakeController(); - controller1.textureId = 101; - await tester.pumpWidget(VideoPlayer(controller1)); - expect( - find.byWidgetPredicate( - (Widget widget) => widget is Texture && widget.textureId == 101, - ), - findsOneWidget); - - final FakeController controller2 = FakeController(); - controller2.textureId = 102; - await tester.pumpWidget(VideoPlayer(controller2)); - expect( - find.byWidgetPredicate( - (Widget widget) => widget is Texture && widget.textureId == 102, - ), - findsOneWidget); - }); - - group('VideoPlayerController', () { - FakeVideoPlayerPlatform fakeVideoPlayerPlatform; - - setUp(() { - fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); - }); - - test('initialize asset', () async { - final VideoPlayerController controller = VideoPlayerController.asset( - 'a.avi', - ); - await controller.initialize(); - - expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0], { - 'asset': 'a.avi', - 'package': null, - }); - }); - - test('initialize network', () async { - final VideoPlayerController controller = VideoPlayerController.network( - 'https://127.0.0.1', - ); - await controller.initialize(); - - expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0], { - 'uri': 'https://127.0.0.1', - 'formatHint': null, - }); - }); - - test('initialize network with hint', () async { - final VideoPlayerController controller = VideoPlayerController.network( - 'https://127.0.0.1', - formatHint: VideoFormat.dash); - await controller.initialize(); - - expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0], { - 'uri': 'https://127.0.0.1', - 'formatHint': 'dash', - }); - }); - - test('initialize file', () async { - final VideoPlayerController controller = - VideoPlayerController.file(File('a.avi')); - await controller.initialize(); - - expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0], { - 'uri': 'file://a.avi', - }); - }); - }); -} - -class FakeVideoPlayerPlatform { - FakeVideoPlayerPlatform() { - _channel.setMockMethodCallHandler(onMethodCall); - } - - final MethodChannel _channel = const MethodChannel('flutter.io/videoPlayer'); - - Completer initialized = Completer(); - List> dataSourceDescriptions = >[]; - int nextTextureId = 0; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'init': - initialized.complete(true); - break; - case 'create': - FakeVideoEventStream( - nextTextureId, 100, 100, const Duration(seconds: 1)); - final Map dataSource = call.arguments; - dataSourceDescriptions.add(dataSource.cast()); - return Future>.sync(() { - return { - 'textureId': nextTextureId++, - }; - }); - break; - case 'setLooping': - break; - case 'setVolume': - break; - case 'pause': - break; - default: - throw UnimplementedError( - '${call.method} is not implemented by the FakeVideoPlayerPlatform'); - } - return Future.sync(() {}); - } -} - -class FakeVideoEventStream { - FakeVideoEventStream(this.textureId, this.width, this.height, this.duration) { - eventsChannel = FakeEventsChannel( - 'flutter.io/videoPlayer/videoEvents$textureId', onListen); - } - - int textureId; - int width; - int height; - Duration duration; - FakeEventsChannel eventsChannel; - - void onListen() { - final Map initializedEvent = { - 'event': 'initialized', - 'duration': duration.inMilliseconds, - 'width': width, - 'height': height, - }; - eventsChannel.sendEvent(initializedEvent); - } -} - -class FakeEventsChannel { - FakeEventsChannel(String name, this.onListen) { - eventsMethodChannel = MethodChannel(name); - eventsMethodChannel.setMockMethodCallHandler(onMethodCall); - } - - MethodChannel eventsMethodChannel; - VoidCallback onListen; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'listen': - onListen(); - break; - } - return Future.sync(() {}); - } - - void sendEvent(dynamic event) { - // TODO(jackson): This has been deprecated and should be replaced - // with `ServicesBinding.instance.defaultBinaryMessenger` when it's - // available on all the versions of Flutter that we test. - // ignore: deprecated_member_use - defaultBinaryMessenger.handlePlatformMessage( - eventsMethodChannel.name, - const StandardMethodCodec().encodeSuccessEnvelope(event), - (ByteData data) {}); - } -} diff --git a/packages/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md similarity index 92% rename from packages/video_player/CHANGELOG.md rename to packages/video_player/video_player/CHANGELOG.md index d4882496320f..d4824c461a55 100644 --- a/packages/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,22 @@ +## 0.10.3+3 + +* Add DartDocs and unit tests. + +## 0.10.3+2 + +* Update the homepage to point to the new plugin location + +## 0.10.3+1 + +* Dispose `FLTVideoPlayer` in `onTextureUnregistered` callback on iOS. +* Add a temporary fix to dispose the `FLTVideoPlayer` with a delay to avoid race condition. +* Updated the example app to include a new page that pop back after video is done playing. + +## 0.10.3 + +* Add support for the v2 Android embedding. This shouldn't impact existing + functionality. + ## 0.10.2+6 * Remove AndroidX warnings. diff --git a/packages/video_player/video_player/LICENSE b/packages/video_player/video_player/LICENSE new file mode 100644 index 000000000000..c89293372cf3 --- /dev/null +++ b/packages/video_player/video_player/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/video_player/README.md b/packages/video_player/video_player/README.md similarity index 97% rename from packages/video_player/README.md rename to packages/video_player/video_player/README.md index 6b7420600e51..eae8f6cb3a46 100644 --- a/packages/video_player/README.md +++ b/packages/video_player/video_player/README.md @@ -4,7 +4,7 @@ A Flutter plugin for iOS and Android for playing back video on a Widget surface. -![The example app running in iOS](https://github.com/flutter/plugins/blob/master/packages/video_player/doc/demo_ipod.gif?raw=true) +![The example app running in iOS](https://github.com/flutter/plugins/blob/master/packages/video_player/video_player/doc/demo_ipod.gif?raw=true) *Note*: This plugin is still under development, and some APIs might not be available yet. [Feedback welcome](https://github.com/flutter/flutter/issues) and diff --git a/packages/video_player/video_player/analysis_options.yaml b/packages/video_player/video_player/analysis_options.yaml new file mode 100644 index 000000000000..4c1fcb727314 --- /dev/null +++ b/packages/video_player/video_player/analysis_options.yaml @@ -0,0 +1,10 @@ +# This is a temporary file to allow us to land a new set of linter rules in a +# series of manageable patches instead of one gigantic PR. It disables some of +# the new lints that are already failing on this plugin, for this plugin. It +# should be deleted and the failing lints addressed as soon as possible. + +include: ../../../analysis_options.yaml + +analyzer: + errors: + unawaited_futures: ignore diff --git a/packages/video_player/android/build.gradle b/packages/video_player/video_player/android/build.gradle similarity index 50% rename from packages/video_player/android/build.gradle rename to packages/video_player/video_player/android/build.gradle index edbb4c7acce4..4a73ec52e0ad 100644 --- a/packages/video_player/android/build.gradle +++ b/packages/video_player/video_player/android/build.gradle @@ -45,3 +45,29 @@ android { implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.9.6' } } + +// TODO(mklim): Remove this hack once androidx.lifecycle is included on stable. https://github.com/flutter/flutter/issues/42348 +afterEvaluate { + def containsEmbeddingDependencies = false + for (def configuration : configurations.all) { + for (def dependency : configuration.dependencies) { + if (dependency.group == 'io.flutter' && + dependency.name.startsWith('flutter_embedding') && + dependency.isTransitive()) + { + containsEmbeddingDependencies = true + break + } + } + } + if (!containsEmbeddingDependencies) { + android { + dependencies { + def lifecycle_version = "1.1.1" + compileOnly "android.arch.lifecycle:runtime:$lifecycle_version" + compileOnly "android.arch.lifecycle:common:$lifecycle_version" + compileOnly "android.arch.lifecycle:common-java8:$lifecycle_version" + } + } + } +} diff --git a/packages/video_player/android/gradle.properties b/packages/video_player/video_player/android/gradle.properties similarity index 100% rename from packages/video_player/android/gradle.properties rename to packages/video_player/video_player/android/gradle.properties diff --git a/packages/video_player/android/gradle/wrapper/gradle-wrapper.properties b/packages/video_player/video_player/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/video_player/android/gradle/wrapper/gradle-wrapper.properties rename to packages/video_player/video_player/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/video_player/android/settings.gradle b/packages/video_player/video_player/android/settings.gradle similarity index 100% rename from packages/video_player/android/settings.gradle rename to packages/video_player/video_player/android/settings.gradle diff --git a/packages/video_player/android/src/main/AndroidManifest.xml b/packages/video_player/video_player/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/video_player/android/src/main/AndroidManifest.xml rename to packages/video_player/video_player/android/src/main/AndroidManifest.xml diff --git a/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java similarity index 100% rename from packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java rename to packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java new file mode 100644 index 000000000000..43123ef09238 --- /dev/null +++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -0,0 +1,283 @@ +package io.flutter.plugins.videoplayer; + +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; + +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import android.view.Surface; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.Util; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.view.TextureRegistry; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +final class VideoPlayer { + private static final String FORMAT_SS = "ss"; + private static final String FORMAT_DASH = "dash"; + private static final String FORMAT_HLS = "hls"; + private static final String FORMAT_OTHER = "other"; + + private SimpleExoPlayer exoPlayer; + + private Surface surface; + + private final TextureRegistry.SurfaceTextureEntry textureEntry; + + private QueuingEventSink eventSink = new QueuingEventSink(); + + private final EventChannel eventChannel; + + private boolean isInitialized = false; + + VideoPlayer( + Context context, + EventChannel eventChannel, + TextureRegistry.SurfaceTextureEntry textureEntry, + String dataSource, + Result result, + String formatHint) { + this.eventChannel = eventChannel; + this.textureEntry = textureEntry; + + TrackSelector trackSelector = new DefaultTrackSelector(); + exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector); + + Uri uri = Uri.parse(dataSource); + + DataSource.Factory dataSourceFactory; + if (isHTTP(uri)) { + dataSourceFactory = + new DefaultHttpDataSourceFactory( + "ExoPlayer", + null, + DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, + DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, + true); + } else { + dataSourceFactory = new DefaultDataSourceFactory(context, "ExoPlayer"); + } + + MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context); + exoPlayer.prepare(mediaSource); + + setupVideoPlayer(eventChannel, textureEntry, result); + } + + private static boolean isHTTP(Uri uri) { + if (uri == null || uri.getScheme() == null) { + return false; + } + String scheme = uri.getScheme(); + return scheme.equals("http") || scheme.equals("https"); + } + + private MediaSource buildMediaSource( + Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) { + int type; + if (formatHint == null) { + type = Util.inferContentType(uri.getLastPathSegment()); + } else { + switch (formatHint) { + case FORMAT_SS: + type = C.TYPE_SS; + break; + case FORMAT_DASH: + type = C.TYPE_DASH; + break; + case FORMAT_HLS: + type = C.TYPE_HLS; + break; + case FORMAT_OTHER: + type = C.TYPE_OTHER; + break; + default: + type = -1; + break; + } + } + switch (type) { + case C.TYPE_SS: + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), + new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) + .createMediaSource(uri); + case C.TYPE_DASH: + return new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(mediaDataSourceFactory), + new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) + .createMediaSource(uri); + case C.TYPE_HLS: + return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); + case C.TYPE_OTHER: + return new ExtractorMediaSource.Factory(mediaDataSourceFactory) + .setExtractorsFactory(new DefaultExtractorsFactory()) + .createMediaSource(uri); + default: + { + throw new IllegalStateException("Unsupported type: " + type); + } + } + } + + private void setupVideoPlayer( + EventChannel eventChannel, TextureRegistry.SurfaceTextureEntry textureEntry, Result result) { + + eventChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink sink) { + eventSink.setDelegate(sink); + } + + @Override + public void onCancel(Object o) { + eventSink.setDelegate(null); + } + }); + + surface = new Surface(textureEntry.surfaceTexture()); + exoPlayer.setVideoSurface(surface); + setAudioAttributes(exoPlayer); + + exoPlayer.addListener( + new EventListener() { + + @Override + public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { + if (playbackState == Player.STATE_BUFFERING) { + sendBufferingUpdate(); + } else if (playbackState == Player.STATE_READY) { + if (!isInitialized) { + isInitialized = true; + sendInitialized(); + } + } else if (playbackState == Player.STATE_ENDED) { + Map event = new HashMap<>(); + event.put("event", "completed"); + eventSink.success(event); + } + } + + @Override + public void onPlayerError(final ExoPlaybackException error) { + if (eventSink != null) { + eventSink.error("VideoError", "Video player had error " + error, null); + } + } + }); + + Map reply = new HashMap<>(); + reply.put("textureId", textureEntry.id()); + result.success(reply); + } + + void sendBufferingUpdate() { + Map event = new HashMap<>(); + event.put("event", "bufferingUpdate"); + List range = Arrays.asList(0, exoPlayer.getBufferedPosition()); + // iOS supports a list of buffered ranges, so here is a list with a single range. + event.put("values", Collections.singletonList(range)); + eventSink.success(event); + } + + @SuppressWarnings("deprecation") + private static void setAudioAttributes(SimpleExoPlayer exoPlayer) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + exoPlayer.setAudioAttributes( + new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build()); + } else { + exoPlayer.setAudioStreamType(C.STREAM_TYPE_MUSIC); + } + } + + void play() { + exoPlayer.setPlayWhenReady(true); + } + + void pause() { + exoPlayer.setPlayWhenReady(false); + } + + void setLooping(boolean value) { + exoPlayer.setRepeatMode(value ? REPEAT_MODE_ALL : REPEAT_MODE_OFF); + } + + void setVolume(double value) { + float bracketedValue = (float) Math.max(0.0, Math.min(1.0, value)); + exoPlayer.setVolume(bracketedValue); + } + + void seekTo(int location) { + exoPlayer.seekTo(location); + } + + long getPosition() { + return exoPlayer.getCurrentPosition(); + } + + @SuppressWarnings("SuspiciousNameCombination") + private void sendInitialized() { + if (isInitialized) { + Map event = new HashMap<>(); + event.put("event", "initialized"); + event.put("duration", exoPlayer.getDuration()); + + if (exoPlayer.getVideoFormat() != null) { + Format videoFormat = exoPlayer.getVideoFormat(); + int width = videoFormat.width; + int height = videoFormat.height; + int rotationDegrees = videoFormat.rotationDegrees; + // Switch the width/height if video was taken in portrait mode + if (rotationDegrees == 90 || rotationDegrees == 270) { + width = exoPlayer.getVideoFormat().height; + height = exoPlayer.getVideoFormat().width; + } + event.put("width", width); + event.put("height", height); + } + eventSink.success(event); + } + } + + void dispose() { + if (isInitialized) { + exoPlayer.stop(); + } + textureEntry.release(); + eventChannel.setStreamHandler(null); + if (surface != null) { + surface.release(); + } + if (exoPlayer != null) { + exoPlayer.release(); + } + } +} diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java new file mode 100644 index 000000000000..fdc0511e217b --- /dev/null +++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -0,0 +1,231 @@ +// 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.videoplayer; + +import android.content.Context; +import android.util.Log; +import android.util.LongSparseArray; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.PluginRegistry.Registrar; +import io.flutter.view.FlutterMain; +import io.flutter.view.TextureRegistry; + +/** Android platform implementation of the VideoPlayerPlugin. */ +public class VideoPlayerPlugin implements MethodCallHandler, FlutterPlugin { + private static final String TAG = "VideoPlayerPlugin"; + private final LongSparseArray videoPlayers = new LongSparseArray<>(); + private FlutterState flutterState; + + /** Register this with the v2 embedding for the plugin to respond to lifecycle callbacks. */ + public VideoPlayerPlugin() {} + + private VideoPlayerPlugin(Registrar registrar) { + this.flutterState = + new FlutterState( + registrar.context(), + registrar.messenger(), + registrar::lookupKeyForAsset, + registrar::lookupKeyForAsset, + registrar.textures()); + flutterState.startListening(this); + } + + /** Registers this with the stable v1 embedding. Will not respond to lifecycle events. */ + public static void registerWith(Registrar registrar) { + final VideoPlayerPlugin plugin = new VideoPlayerPlugin(registrar); + registrar.addViewDestroyListener( + view -> { + plugin.onDestroy(); + return false; // We are not interested in assuming ownership of the NativeView. + }); + } + + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + this.flutterState = + new FlutterState( + binding.getApplicationContext(), + binding.getFlutterEngine().getDartExecutor(), + FlutterMain::getLookupKeyForAsset, + FlutterMain::getLookupKeyForAsset, + binding.getFlutterEngine().getRenderer()); + flutterState.startListening(this); + } + + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) { + if (flutterState == null) { + Log.wtf(TAG, "Detached from the engine before registering to it."); + } + flutterState.stopListening(); + flutterState = null; + } + + private void disposeAllPlayers() { + for (int i = 0; i < videoPlayers.size(); i++) { + videoPlayers.valueAt(i).dispose(); + } + videoPlayers.clear(); + } + + private void onDestroy() { + // The whole FlutterView is being destroyed. Here we release resources acquired for all + // instances + // of VideoPlayer. Once https://github.com/flutter/flutter/issues/19358 is resolved this may + // be replaced with just asserting that videoPlayers.isEmpty(). + // https://github.com/flutter/flutter/issues/20989 tracks this. + disposeAllPlayers(); + } + + @Override + public void onMethodCall(MethodCall call, Result result) { + if (flutterState == null || flutterState.textureRegistry == null) { + result.error("no_activity", "video_player plugin requires a foreground activity", null); + return; + } + switch (call.method) { + case "init": + disposeAllPlayers(); + break; + case "create": + { + TextureRegistry.SurfaceTextureEntry handle = + flutterState.textureRegistry.createSurfaceTexture(); + EventChannel eventChannel = + new EventChannel( + flutterState.binaryMessenger, "flutter.io/videoPlayer/videoEvents" + handle.id()); + + VideoPlayer player; + if (call.argument("asset") != null) { + String assetLookupKey; + if (call.argument("package") != null) { + assetLookupKey = + flutterState.keyForAssetAndPackageName.get( + call.argument("asset"), call.argument("package")); + } else { + assetLookupKey = flutterState.keyForAsset.get(call.argument("asset")); + } + player = + new VideoPlayer( + flutterState.applicationContext, + eventChannel, + handle, + "asset:///" + assetLookupKey, + result, + null); + videoPlayers.put(handle.id(), player); + } else { + player = + new VideoPlayer( + flutterState.applicationContext, + eventChannel, + handle, + call.argument("uri"), + result, + call.argument("formatHint")); + videoPlayers.put(handle.id(), player); + } + break; + } + default: + { + long textureId = ((Number) call.argument("textureId")).longValue(); + VideoPlayer player = videoPlayers.get(textureId); + if (player == null) { + result.error( + "Unknown textureId", + "No video player associated with texture id " + textureId, + null); + return; + } + onMethodCall(call, result, textureId, player); + break; + } + } + } + + private void onMethodCall(MethodCall call, Result result, long textureId, VideoPlayer player) { + switch (call.method) { + case "setLooping": + player.setLooping(call.argument("looping")); + result.success(null); + break; + case "setVolume": + player.setVolume(call.argument("volume")); + result.success(null); + break; + case "play": + player.play(); + result.success(null); + break; + case "pause": + player.pause(); + result.success(null); + break; + case "seekTo": + int location = ((Number) call.argument("location")).intValue(); + player.seekTo(location); + result.success(null); + break; + case "position": + result.success(player.getPosition()); + player.sendBufferingUpdate(); + break; + case "dispose": + player.dispose(); + videoPlayers.remove(textureId); + result.success(null); + break; + default: + result.notImplemented(); + break; + } + } + + private interface KeyForAssetFn { + String get(String asset); + } + + private interface KeyForAssetAndPackageName { + String get(String asset, String packageName); + } + + private static final class FlutterState { + private final Context applicationContext; + private final BinaryMessenger binaryMessenger; + private final KeyForAssetFn keyForAsset; + private final KeyForAssetAndPackageName keyForAssetAndPackageName; + private final TextureRegistry textureRegistry; + private final MethodChannel methodChannel; + + FlutterState( + Context applicationContext, + BinaryMessenger messenger, + KeyForAssetFn keyForAsset, + KeyForAssetAndPackageName keyForAssetAndPackageName, + TextureRegistry textureRegistry) { + this.applicationContext = applicationContext; + this.binaryMessenger = messenger; + this.keyForAsset = keyForAsset; + this.keyForAssetAndPackageName = keyForAssetAndPackageName; + this.textureRegistry = textureRegistry; + methodChannel = new MethodChannel(messenger, "flutter.io/videoPlayer"); + } + + void startListening(VideoPlayerPlugin methodCallHandler) { + methodChannel.setMethodCallHandler(methodCallHandler); + } + + void stopListening() { + methodChannel.setMethodCallHandler(null); + } + } +} diff --git a/packages/video_player/doc/demo_ipod.gif b/packages/video_player/video_player/doc/demo_ipod.gif similarity index 100% rename from packages/video_player/doc/demo_ipod.gif rename to packages/video_player/video_player/doc/demo_ipod.gif diff --git a/packages/video_player/example/README.md b/packages/video_player/video_player/example/README.md similarity index 100% rename from packages/video_player/example/README.md rename to packages/video_player/video_player/example/README.md diff --git a/packages/shared_preferences/example/android.iml b/packages/video_player/video_player/example/android.iml similarity index 100% rename from packages/shared_preferences/example/android.iml rename to packages/video_player/video_player/example/android.iml diff --git a/packages/video_player/example/android/app/build.gradle b/packages/video_player/video_player/example/android/app/build.gradle similarity index 100% rename from packages/video_player/example/android/app/build.gradle rename to packages/video_player/video_player/example/android/app/build.gradle diff --git a/packages/video_player/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/video_player/video_player/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/video_player/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/video_player/video_player/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/video_player/video_player/example/android/app/src/main/AndroidManifest.xml b/packages/video_player/video_player/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..deec4b6b5b08 --- /dev/null +++ b/packages/video_player/video_player/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + diff --git a/packages/video_player/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java b/packages/video_player/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java new file mode 100644 index 000000000000..f1af8ecd74e7 --- /dev/null +++ b/packages/video_player/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java @@ -0,0 +1,17 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.videoplayerexample; + +import android.os.Bundle; +import io.flutter.app.FlutterActivity; +import io.flutter.plugins.GeneratedPluginRegistrant; + +public class EmbeddingV1Activity extends FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + GeneratedPluginRegistrant.registerWith(this); + } +} diff --git a/packages/video_player/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java b/packages/video_player/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java new file mode 100644 index 000000000000..2a0ae15e5e2f --- /dev/null +++ b/packages/video_player/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java @@ -0,0 +1,16 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.videoplayerexample; + +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugins.videoplayer.VideoPlayerPlugin; + +public class MainActivity extends FlutterActivity { + @Override + public void configureFlutterEngine(FlutterEngine flutterEngine) { + flutterEngine.getPlugins().add(new VideoPlayerPlugin()); + } +} diff --git a/packages/video_player/example/android/app/src/main/res/drawable/launch_background.xml b/packages/video_player/video_player/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from packages/video_player/example/android/app/src/main/res/drawable/launch_background.xml rename to packages/video_player/video_player/example/android/app/src/main/res/drawable/launch_background.xml diff --git a/packages/video_player/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/video_player/video_player/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/video_player/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/video_player/video_player/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/video_player/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/video_player/video_player/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/video_player/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/video_player/video_player/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/video_player/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/video_player/video_player/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/video_player/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/video_player/video_player/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/video_player/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/video_player/video_player/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/video_player/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/video_player/video_player/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/video_player/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/video_player/video_player/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/video_player/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/video_player/video_player/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/video_player/example/android/app/src/main/res/values/styles.xml b/packages/video_player/video_player/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from packages/video_player/example/android/app/src/main/res/values/styles.xml rename to packages/video_player/video_player/example/android/app/src/main/res/values/styles.xml diff --git a/packages/video_player/example/android/app/src/main/res/xml/network_security_config.xml b/packages/video_player/video_player/example/android/app/src/main/res/xml/network_security_config.xml similarity index 100% rename from packages/video_player/example/android/app/src/main/res/xml/network_security_config.xml rename to packages/video_player/video_player/example/android/app/src/main/res/xml/network_security_config.xml diff --git a/packages/video_player/example/android/build.gradle b/packages/video_player/video_player/example/android/build.gradle similarity index 100% rename from packages/video_player/example/android/build.gradle rename to packages/video_player/video_player/example/android/build.gradle diff --git a/packages/video_player/video_player/example/android/gradle.properties b/packages/video_player/video_player/example/android/gradle.properties new file mode 100644 index 000000000000..a6738207fd15 --- /dev/null +++ b/packages/video_player/video_player/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +android.enableR8=true diff --git a/packages/video_player/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/video_player/video_player/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/video_player/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/video_player/video_player/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/video_player/example/android/settings.gradle b/packages/video_player/video_player/example/android/settings.gradle similarity index 100% rename from packages/video_player/example/android/settings.gradle rename to packages/video_player/video_player/example/android/settings.gradle diff --git a/packages/video_player/example/assets/Butterfly-209.mp4 b/packages/video_player/video_player/example/assets/Butterfly-209.mp4 similarity index 100% rename from packages/video_player/example/assets/Butterfly-209.mp4 rename to packages/video_player/video_player/example/assets/Butterfly-209.mp4 diff --git a/packages/video_player/example/assets/flutter-mark-square-64.png b/packages/video_player/video_player/example/assets/flutter-mark-square-64.png similarity index 100% rename from packages/video_player/example/assets/flutter-mark-square-64.png rename to packages/video_player/video_player/example/assets/flutter-mark-square-64.png diff --git a/packages/video_player/example/ios/Flutter/AppFrameworkInfo.plist b/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from packages/video_player/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/packages/video_player/example/ios/Flutter/Debug.xcconfig b/packages/video_player/video_player/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/video_player/example/ios/Flutter/Debug.xcconfig rename to packages/video_player/video_player/example/ios/Flutter/Debug.xcconfig diff --git a/packages/video_player/example/ios/Flutter/Release.xcconfig b/packages/video_player/video_player/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/video_player/example/ios/Flutter/Release.xcconfig rename to packages/video_player/video_player/example/ios/Flutter/Release.xcconfig diff --git a/packages/video_player/example/ios/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from packages/video_player/example/ios/Runner.xcodeproj/project.pbxproj rename to packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj diff --git a/packages/video_player/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/video_player/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/video_player/video_player/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/packages/shared_preferences/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/video_player/video_player/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 95% rename from packages/shared_preferences/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/video_player/video_player/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1c9580788197..3bb3697ef41c 100644 --- a/packages/shared_preferences/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/video_player/video_player/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + - - { _VideoPlayPauseState() { listener = () { - setState(() {}); + SchedulerBinding.instance.addPostFrameCallback((_) => setState(() {})); }; } @@ -48,8 +51,11 @@ class _VideoPlayPauseState extends State { @override void deactivate() { - controller.setVolume(0.0); - controller.removeListener(listener); + SchedulerBinding.instance.addPostFrameCallback((_) { + controller.setVolume(0.0); + controller.removeListener(listener); + }); + super.deactivate(); } @@ -360,73 +366,152 @@ class AspectRatioVideoState extends State { } } -void main() { - runApp( - MaterialApp( - home: DefaultTabController( - length: 3, - child: Scaffold( - appBar: AppBar( - title: const Text('Video player example'), - bottom: const TabBar( - isScrollable: true, - tabs: [ - Tab( - icon: Icon(Icons.cloud), - text: "Remote", - ), - Tab(icon: Icon(Icons.insert_drive_file), text: "Asset"), - Tab(icon: Icon(Icons.list), text: "List example"), - ], - ), +class App extends StatelessWidget { + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 3, + child: Scaffold( + key: const ValueKey('home_page'), + appBar: AppBar( + title: const Text('Video player example'), + actions: [ + IconButton( + key: const ValueKey('push_tab'), + icon: const Icon(Icons.navigation), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + PlayerVideoAndPopPage()), + ); + }, + ) + ], + bottom: const TabBar( + isScrollable: true, + tabs: [ + Tab( + icon: Icon(Icons.cloud), + text: "Remote", + ), + Tab(icon: Icon(Icons.insert_drive_file), text: "Asset"), + Tab(icon: Icon(Icons.list), text: "List example"), + ], ), - body: TabBarView( - children: [ - SingleChildScrollView( - child: Column( - children: [ - Container( - padding: const EdgeInsets.only(top: 20.0), + ), + body: TabBarView( + children: [ + SingleChildScrollView( + child: Column( + children: [ + Container( + padding: const EdgeInsets.only(top: 20.0), + ), + const Text('With remote mp4'), + Container( + padding: const EdgeInsets.all(20), + child: NetworkPlayerLifeCycle( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + (BuildContext context, + VideoPlayerController controller) => + AspectRatioVideo(controller), ), - const Text('With remote m3u8'), - Container( - padding: const EdgeInsets.all(20), - child: NetworkPlayerLifeCycle( - 'http://184.72.239.149/vod/smil:BigBuckBunny.smil/playlist.m3u8', + ), + ], + ), + ), + SingleChildScrollView( + child: Column( + children: [ + Container( + padding: const EdgeInsets.only(top: 20.0), + ), + const Text('With assets mp4'), + Container( + padding: const EdgeInsets.all(20), + child: AssetPlayerLifeCycle( + 'assets/Butterfly-209.mp4', (BuildContext context, VideoPlayerController controller) => - AspectRatioVideo(controller), - ), - ), - ], - ), + AspectRatioVideo(controller)), + ), + ], ), - SingleChildScrollView( - child: Column( - children: [ - Container( - padding: const EdgeInsets.only(top: 20.0), - ), - const Text('With assets mp4'), - Container( - padding: const EdgeInsets.all(20), - child: AssetPlayerLifeCycle( - 'assets/Butterfly-209.mp4', - (BuildContext context, - VideoPlayerController controller) => - AspectRatioVideo(controller)), - ), - ], - ), - ), - AssetPlayerLifeCycle( - 'assets/Butterfly-209.mp4', - (BuildContext context, VideoPlayerController controller) => - VideoInListOfCards(controller)), - ], - ), + ), + AssetPlayerLifeCycle( + 'assets/Butterfly-209.mp4', + (BuildContext context, VideoPlayerController controller) => + VideoInListOfCards(controller)), + ], ), ), + ); + } +} + +void main() { + runApp( + MaterialApp( + home: App(), ), ); } + +class PlayerVideoAndPopPage extends StatefulWidget { + @override + _PlayerVideoAndPopPageState createState() => _PlayerVideoAndPopPageState(); +} + +class _PlayerVideoAndPopPageState extends State { + VideoPlayerController _videoPlayerController; + bool startedPlaying = false; + + @override + void initState() { + super.initState(); + + _videoPlayerController = + VideoPlayerController.asset('assets/Butterfly-209.mp4'); + _videoPlayerController.addListener(() { + if (startedPlaying && !_videoPlayerController.value.isPlaying) { + Navigator.pop(context); + } + }); + } + + @override + void dispose() { + _videoPlayerController.dispose(); + super.dispose(); + } + + Future started() async { + await _videoPlayerController.initialize(); + await _videoPlayerController.play(); + startedPlaying = true; + return true; + } + + @override + Widget build(BuildContext context) { + return Material( + elevation: 0, + child: Center( + child: FutureBuilder( + future: started(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.data == true) { + return AspectRatio( + aspectRatio: _videoPlayerController.value.aspectRatio, + child: VideoPlayer(_videoPlayerController)); + } else { + return const Text('waiting for video to load'); + } + }, + ), + ), + ); + } +} diff --git a/packages/video_player/example/pubspec.yaml b/packages/video_player/video_player/example/pubspec.yaml similarity index 84% rename from packages/video_player/example/pubspec.yaml rename to packages/video_player/video_player/example/pubspec.yaml index da48f06f0796..b83e8d177646 100644 --- a/packages/video_player/example/pubspec.yaml +++ b/packages/video_player/video_player/example/pubspec.yaml @@ -4,13 +4,16 @@ description: Demonstrates how to use the video_player plugin. dependencies: flutter: sdk: flutter + video_player: + path: ../ dev_dependencies: flutter_test: sdk: flutter - - video_player: - path: ../ + flutter_driver: + sdk: flutter + e2e: "^0.2.0" + test: any flutter: uses-material-design: true diff --git a/packages/video_player/video_player/example/test_driver/video_player_e2e.dart b/packages/video_player/video_player/example/test_driver/video_player_e2e.dart new file mode 100644 index 000000000000..bf35cf50b728 --- /dev/null +++ b/packages/video_player/video_player/example/test_driver/video_player_e2e.dart @@ -0,0 +1,66 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; +import 'package:e2e/e2e.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:video_player/video_player.dart'; + +const Duration _playDuration = Duration(seconds: 1); + +void main() { + E2EWidgetsFlutterBinding.ensureInitialized(); + VideoPlayerController _controller; + tearDown(() async => _controller.dispose()); + + group('asset videos', () { + setUp(() { + _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4'); + }); + + testWidgets('can be initialized', (WidgetTester tester) async { + await _controller.initialize(); + + expect(_controller.value.initialized, true); + expect(_controller.value.position, const Duration(seconds: 0)); + expect(_controller.value.isPlaying, false); + expect(_controller.value.duration, + const Duration(seconds: 7, milliseconds: 540)); + }); + + testWidgets('can be played', (WidgetTester tester) async { + await _controller.initialize(); + + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + + expect(_controller.value.isPlaying, true); + expect(_controller.value.position, + (Duration position) => position > const Duration(seconds: 0)); + }, skip: Platform.isIOS); + + testWidgets('can seek', (WidgetTester tester) async { + await _controller.initialize(); + + await _controller.seekTo(const Duration(seconds: 3)); + + expect(_controller.value.position, const Duration(seconds: 3)); + }, skip: Platform.isIOS); + + testWidgets('can be paused', (WidgetTester tester) async { + await _controller.initialize(); + + // Play for a second, then pause, and then wait a second. + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + await _controller.pause(); + final Duration pausedPosition = _controller.value.position; + await tester.pumpAndSettle(_playDuration); + + // Verify that we stopped playing after the pause. + expect(_controller.value.isPlaying, false); + expect(_controller.value.position, pausedPosition); + }, skip: Platform.isIOS); + }); +} diff --git a/packages/video_player/video_player/example/test_driver/video_player_e2e_test.dart b/packages/video_player/video_player/example/test_driver/video_player_e2e_test.dart new file mode 100644 index 000000000000..f3aa9e218d82 --- /dev/null +++ b/packages/video_player/video_player/example/test_driver/video_player_e2e_test.dart @@ -0,0 +1,15 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; +import 'package:flutter_driver/flutter_driver.dart'; + +Future main() async { + final FlutterDriver driver = await FlutterDriver.connect(); + final String result = + await driver.requestData(null, timeout: const Duration(minutes: 1)); + await driver.close(); + exit(result == 'pass' ? 0 : 1); +} diff --git a/packages/video_player/example/video_player_example.iml b/packages/video_player/video_player/example/video_player_example.iml similarity index 100% rename from packages/video_player/example/video_player_example.iml rename to packages/video_player/video_player/example/video_player_example.iml diff --git a/packages/video_player/example/video_player_example_android.iml b/packages/video_player/video_player/example/video_player_example_android.iml similarity index 100% rename from packages/video_player/example/video_player_example_android.iml rename to packages/video_player/video_player/example/video_player_example_android.iml diff --git a/packages/video_player/ios/Assets/.gitkeep b/packages/video_player/video_player/ios/Assets/.gitkeep similarity index 100% rename from packages/video_player/ios/Assets/.gitkeep rename to packages/video_player/video_player/ios/Assets/.gitkeep diff --git a/packages/video_player/ios/Classes/VideoPlayerPlugin.h b/packages/video_player/video_player/ios/Classes/VideoPlayerPlugin.h similarity index 100% rename from packages/video_player/ios/Classes/VideoPlayerPlugin.h rename to packages/video_player/video_player/ios/Classes/VideoPlayerPlugin.h diff --git a/packages/video_player/ios/Classes/VideoPlayerPlugin.m b/packages/video_player/video_player/ios/Classes/VideoPlayerPlugin.m similarity index 94% rename from packages/video_player/ios/Classes/VideoPlayerPlugin.m rename to packages/video_player/video_player/ios/Classes/VideoPlayerPlugin.m index bf449ec0e8e2..9bf61d8fed14 100644 --- a/packages/video_player/ios/Classes/VideoPlayerPlugin.m +++ b/packages/video_player/video_player/ios/Classes/VideoPlayerPlugin.m @@ -363,6 +363,12 @@ - (CVPixelBufferRef)copyPixelBuffer { } } +- (void)onTextureUnregistered { + dispatch_async(dispatch_get_main_queue(), ^{ + [self dispose]; + }); +} + - (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments { _eventSink = nil; return nil; @@ -487,7 +493,22 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if ([@"dispose" isEqualToString:call.method]) { [_registry unregisterTexture:textureId]; [_players removeObjectForKey:@(textureId)]; - [player dispose]; + // If the Flutter contains https://github.com/flutter/engine/pull/12695, + // the `player` is disposed via `onTextureUnregistered` at the right time. + // Without https://github.com/flutter/engine/pull/12695, there is no guarantee that the + // texture has completed the un-reregistration. It may leads a crash if we dispose the + // `player` before the texture is unregistered. We add a dispatch_after hack to make sure the + // texture is unregistered before we dispose the `player`. + // + // TODO(cyanglaz): Remove this dispatch block when + // https://github.com/flutter/flutter/commit/8159a9906095efc9af8b223f5e232cb63542ad0b is in + // stable And update the min flutter version of the plugin to the stable version. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + if (!player.disposed) { + [player dispose]; + } + }); result(nil); } else if ([@"setLooping" isEqualToString:call.method]) { [player setIsLooping:[argsMap[@"looping"] boolValue]]; diff --git a/packages/video_player/ios/video_player.podspec b/packages/video_player/video_player/ios/video_player.podspec similarity index 100% rename from packages/video_player/ios/video_player.podspec rename to packages/video_player/video_player/ios/video_player.podspec diff --git a/packages/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart similarity index 74% rename from packages/video_player/lib/video_player.dart rename to packages/video_player/video_player/lib/video_player.dart index f1b0e7c9791d..f92693e79eda 100644 --- a/packages/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -14,16 +14,46 @@ final MethodChannel _channel = const MethodChannel('flutter.io/videoPlayer') // performed. ..invokeMethod('init'); +/// Describes a discrete segment of time within a video using a [start] and +/// [end] [Duration]. class DurationRange { + /// Trusts that the given [start] and [end] are actually in order. They should + /// both be non-null. DurationRange(this.start, this.end); + /// The beginning of the segment described relative to the beginning of the + /// entire video. Should be shorter than or equal to [end]. + /// + /// For example, if the entire video is 4 minutes long and the range is from + /// 1:00-2:00, this should be a `Duration` of one minute. final Duration start; + + /// The end of the segment described as a duration relative to the beginning of + /// the entire video. This is expected to be non-null and longer than or equal + /// to [start]. + /// + /// For example, if the entire video is 4 minutes long and the range is from + /// 1:00-2:00, this should be a `Duration` of two minutes. final Duration end; + /// Assumes that [duration] is the total length of the video that this + /// DurationRange is a segment form. It returns the percentage that [start] is + /// through the entire video. + /// + /// For example, assume that the entire video is 4 minutes long. If [start] has + /// a duration of one minute, this will return `0.25` since the DurationRange + /// starts 25% of the way through the video's total length. double startFraction(Duration duration) { return start.inMilliseconds / duration.inMilliseconds; } + /// Assumes that [duration] is the total length of the video that this + /// DurationRange is a segment form. It returns the percentage that [start] is + /// through the entire video. + /// + /// For example, assume that the entire video is 4 minutes long. If [end] has a + /// duration of two minutes, this will return `0.5` since the DurationRange + /// ends 50% of the way through the video's total length. double endFraction(Duration duration) { return end.inMilliseconds / duration.inMilliseconds; } @@ -32,11 +62,26 @@ class DurationRange { String toString() => '$runtimeType(start: $start, end: $end)'; } -enum VideoFormat { dash, hls, ss, other } +/// The file format of the given video. +enum VideoFormat { + /// Dynamic Adaptive Streaming over HTTP, also known as MPEG-DASH. + dash, + + /// HTTP Live Streaming. + hls, + + /// Smooth Streaming. + ss, + + /// Any format other than the other ones defined in this enum. + other +} /// The duration, current position, buffering state, error state and settings /// of a [VideoPlayerController]. class VideoPlayerValue { + /// Constructs a video with the given values. Only [duration] is required. The + /// rest will initialize with default values when unset. VideoPlayerValue({ @required this.duration, this.size, @@ -49,8 +94,11 @@ class VideoPlayerValue { this.errorDescription, }); + /// Returns an instance with a `null` [Duration]. VideoPlayerValue.uninitialized() : this(duration: null); + /// Returns an instance with a `null` [Duration] and the given + /// [errorDescription]. VideoPlayerValue.erroneous(String errorDescription) : this(duration: null, errorDescription: errorDescription); @@ -87,10 +135,19 @@ class VideoPlayerValue { /// Is null when [initialized] is false. final Size size; + /// Indicates whether or not the video has been loaded and is ready to play. bool get initialized => duration != null; + + /// Indicates whether or not the video is in an error state. If this is true + /// [errorDescription] should have information about the problem. bool get hasError => errorDescription != null; + + /// Returns [size.width] / [size.height] when size is non-null, or `1.0.` when + /// it is. double get aspectRatio => size != null ? size.width / size.height : 1.0; + /// Returns a new instance that has the same values as this current instance, + /// except for any overrides passed in as arguments to [copyWidth]. VideoPlayerValue copyWith({ Duration duration, Size size, @@ -130,7 +187,19 @@ class VideoPlayerValue { } } -enum DataSourceType { asset, network, file } +/// The way in which the video was originally loaded. This has nothing to do +/// with the video's file type. It's just the place from which the video is +/// fetched from. +enum DataSourceType { + /// The video was included in the app's asset files. + asset, + + /// The video was downloaded from the internet. + network, + + /// The video was loaded off of the local filesystem. + file +} /// Controls a platform video player, and provides updates when the state is /// changing. @@ -177,13 +246,20 @@ class VideoPlayerController extends ValueNotifier { super(VideoPlayerValue(duration: null)); int _textureId; + + /// The URI to the video file. This will be in different formats depending on + /// the [DataSourceType] of the original video. final String dataSource; + + /// **Android only**. Will override the platform's generic file format + /// detection with whatever is set here. final VideoFormat formatHint; /// Describes the type of data source this [VideoPlayerController] /// is constructed with. final DataSourceType dataSourceType; + /// Only set for [asset] videos. The package that the asset was loaded from. final String package; Timer _timer; bool _isDisposed = false; @@ -191,9 +267,12 @@ class VideoPlayerController extends ValueNotifier { StreamSubscription _eventSubscription; _VideoAppLifeCycleObserver _lifeCycleObserver; + /// This is just exposed for testing. It shouldn't be used by anyone depending + /// on the plugin. @visibleForTesting int get textureId => _textureId; + /// Attempts to open the given [dataSource] and load metadata about the video. Future initialize() async { _lifeCycleObserver = _VideoAppLifeCycleObserver(this); _lifeCycleObserver.initialize(); @@ -305,16 +384,24 @@ class VideoPlayerController extends ValueNotifier { super.dispose(); } + /// Starts playing the video. + /// + /// This method returns a future that completes as soon as the "play" command + /// has been sent to the platform, not when playback itself is totally + /// finished. Future play() async { value = value.copyWith(isPlaying: true); await _applyPlayPause(); } + /// Sets whether or not the video should loop after playing once. See also + /// [VideoPlayerValue.isLooping]. Future setLooping(bool looping) async { value = value.copyWith(isLooping: looping); await _applyLooping(); } + /// Pauses the video. Future pause() async { value = value.copyWith(isPlaying: false); await _applyPlayPause(); @@ -384,6 +471,11 @@ class VideoPlayerController extends ValueNotifier { ); } + /// Sets the video's current timestamp to be at [moment]. The next + /// time the video is played it will resume from the given [moment]. + /// + /// If [moment] is outside of the video's full range it will be automatically + /// and silently clamped. Future seekTo(Duration moment) async { if (_isDisposed) { return; @@ -449,10 +541,13 @@ class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { } } -/// Displays the video controlled by [controller]. +/// Widget that displays the video controlled by [controller]. class VideoPlayer extends StatefulWidget { + /// Uses the given [controller] for all video rendered in this widget. VideoPlayer(this.controller); + /// The [VideoPlayerController] responsible for the video being rendered in + /// this widget. final VideoPlayerController controller; @override @@ -503,15 +598,43 @@ class _VideoPlayerState extends State { } } +/// Used to configure the [VideoProgressIndicator] widget's colors for how it +/// describes the video's status. +/// +/// The widget uses default colors that are customizeable through this class. class VideoProgressColors { + /// Any property can be set to any color. They each have defaults. + /// + /// [playedColor] defaults to red at 70% opacity. This fills up a portion of + /// the [VideoProgressIndicator] to represent how much of the video has played + /// so far. + /// + /// [bufferedColor] defaults to blue at 20% opacity. This fills up a portion + /// of [VideoProgressIndicator] to represent how much of the video has + /// buffered so far. + /// + /// [backgroundColor] defaults to gray at 50% opacity. This is the background + /// color behind both [playedColor] and [bufferedColor] to denote the total + /// size of the video compared to either of those values. VideoProgressColors({ this.playedColor = const Color.fromRGBO(255, 0, 0, 0.7), this.bufferedColor = const Color.fromRGBO(50, 50, 200, 0.2), this.backgroundColor = const Color.fromRGBO(200, 200, 200, 0.5), }); + /// [playedColor] defaults to red at 70% opacity. This fills up a portion of + /// the [VideoProgressIndicator] to represent how much of the video has played + /// so far. final Color playedColor; + + /// [bufferedColor] defaults to blue at 20% opacity. This fills up a portion + /// of [VideoProgressIndicator] to represent how much of the video has + /// buffered so far. final Color bufferedColor; + + /// [backgroundColor] defaults to gray at 50% opacity. This is the background + /// color behind both [playedColor] and [bufferedColor] to denote the total + /// size of the video compared to either of those values. final Color backgroundColor; } @@ -584,6 +707,12 @@ class _VideoScrubberState extends State<_VideoScrubber> { /// [padding] allows to specify some extra padding around the progress indicator /// that will also detect the gestures. class VideoProgressIndicator extends StatefulWidget { + /// Construct an instance that displays the play/buffering status of the video + /// controlled by [controller]. + /// + /// Defaults will be used for everything except [controller] if they're not + /// provided. [allowScrubbing] defaults to false, and [padding] will default + /// to `top: 5.0`. VideoProgressIndicator( this.controller, { VideoProgressColors colors, @@ -591,9 +720,25 @@ class VideoProgressIndicator extends StatefulWidget { this.padding = const EdgeInsets.only(top: 5.0), }) : colors = colors ?? VideoProgressColors(); + /// The [VideoPlayerController] that actually associates a video with this + /// widget. final VideoPlayerController controller; + + /// The default colors used throughout the indicator. + /// + /// See [VideoProgressColors] for default values. final VideoProgressColors colors; + + /// When true, the widget will detect touch input and try to seek the video + /// accordingly. The widget ignores such input when false. + /// + /// Defaults to false. final bool allowScrubbing; + + /// This allows for visual padding around the progress indicator that can + /// still detect gestures via [allowScrubbing]. + /// + /// Defaults to `top: 5.0`. final EdgeInsets padding; @override diff --git a/packages/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml similarity index 90% rename from packages/video_player/pubspec.yaml rename to packages/video_player/video_player/pubspec.yaml index 1b0805be1764..cf85568de389 100644 --- a/packages/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player description: Flutter plugin for displaying inline video with other Flutter widgets on Android and iOS. author: Flutter Team -version: 0.10.2+6 +version: 0.10.3+3 homepage: https://github.com/flutter/plugins/tree/master/packages/video_player flutter: @@ -22,4 +22,4 @@ dev_dependencies: environment: sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.5.0 <2.0.0" + flutter: ">=1.9.1+hotfix.5 <2.0.0" diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart new file mode 100644 index 000000000000..10b5754bbad2 --- /dev/null +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -0,0 +1,533 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:video_player/video_player.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FakeController extends ValueNotifier + implements VideoPlayerController { + FakeController() : super(VideoPlayerValue(duration: null)); + + @override + Future dispose() async { + super.dispose(); + } + + @override + int textureId; + + @override + String get dataSource => ''; + @override + DataSourceType get dataSourceType => DataSourceType.file; + @override + String get package => null; + @override + Future get position async => value.position; + + @override + Future seekTo(Duration moment) async {} + @override + Future setVolume(double volume) async {} + @override + Future initialize() async {} + @override + Future pause() async {} + @override + Future play() async {} + @override + Future setLooping(bool looping) async {} + + @override + VideoFormat get formatHint => null; +} + +void main() { + testWidgets('update texture', (WidgetTester tester) async { + final FakeController controller = FakeController(); + await tester.pumpWidget(VideoPlayer(controller)); + expect(find.byType(Texture), findsNothing); + + controller.textureId = 123; + controller.value = controller.value.copyWith( + duration: const Duration(milliseconds: 100), + ); + + await tester.pump(); + expect(find.byType(Texture), findsOneWidget); + }); + + testWidgets('update controller', (WidgetTester tester) async { + final FakeController controller1 = FakeController(); + controller1.textureId = 101; + await tester.pumpWidget(VideoPlayer(controller1)); + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Texture && widget.textureId == 101, + ), + findsOneWidget); + + final FakeController controller2 = FakeController(); + controller2.textureId = 102; + await tester.pumpWidget(VideoPlayer(controller2)); + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Texture && widget.textureId == 102, + ), + findsOneWidget); + }); + + group('VideoPlayerController', () { + FakeVideoPlayerPlatform fakeVideoPlayerPlatform; + + setUp(() { + fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); + }); + + group('initialize', () { + test('asset', () async { + final VideoPlayerController controller = VideoPlayerController.asset( + 'a.avi', + ); + await controller.initialize(); + + expect( + fakeVideoPlayerPlatform.dataSourceDescriptions[0], + { + 'asset': 'a.avi', + 'package': null, + }); + }); + + test('network', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + + expect( + fakeVideoPlayerPlatform.dataSourceDescriptions[0], + { + 'uri': 'https://127.0.0.1', + 'formatHint': null, + }); + }); + + test('network with hint', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + formatHint: VideoFormat.dash); + await controller.initialize(); + + expect( + fakeVideoPlayerPlatform.dataSourceDescriptions[0], + { + 'uri': 'https://127.0.0.1', + 'formatHint': 'dash', + }); + }); + + test('file', () async { + final VideoPlayerController controller = + VideoPlayerController.file(File('a.avi')); + await controller.initialize(); + + expect( + fakeVideoPlayerPlatform.dataSourceDescriptions[0], + { + 'uri': 'file://a.avi', + }); + }); + }); + + test('dispose', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + expect(controller.textureId, isNull); + expect(await controller.position, const Duration(seconds: 0)); + controller.initialize(); + + await controller.dispose(); + + expect(controller.textureId, isNotNull); + expect(await controller.position, isNull); + }); + + test('play', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + expect(controller.value.isPlaying, isFalse); + await controller.play(); + + expect(controller.value.isPlaying, isTrue); + expect(fakeVideoPlayerPlatform.calls.last.method, 'play'); + }); + + test('setLooping', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + expect(controller.value.isLooping, isFalse); + await controller.setLooping(true); + + expect(controller.value.isLooping, isTrue); + }); + + test('pause', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + await controller.play(); + expect(controller.value.isPlaying, isTrue); + + await controller.pause(); + + expect(controller.value.isPlaying, isFalse); + expect(fakeVideoPlayerPlatform.calls.last.method, 'pause'); + }); + + group('seekTo', () { + test('works', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + expect(await controller.position, const Duration(seconds: 0)); + + await controller.seekTo(const Duration(milliseconds: 500)); + + expect(await controller.position, const Duration(milliseconds: 500)); + }); + + test('clamps values that are too high or low', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + expect(await controller.position, const Duration(seconds: 0)); + + await controller.seekTo(const Duration(seconds: 100)); + expect(await controller.position, const Duration(seconds: 1)); + + await controller.seekTo(const Duration(seconds: -100)); + expect(await controller.position, const Duration(seconds: 0)); + }); + }); + + group('setVolume', () { + test('works', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + expect(controller.value.volume, 1.0); + + const double volume = 0.5; + await controller.setVolume(volume); + + expect(controller.value.volume, volume); + }); + + test('clamps values that are too high or low', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + expect(controller.value.volume, 1.0); + + await controller.setVolume(-1); + expect(controller.value.volume, 0.0); + + await controller.setVolume(11); + expect(controller.value.volume, 1.0); + }); + }); + + group('Platform callbacks', () { + testWidgets('playing completed', (WidgetTester tester) async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + expect(controller.value.isPlaying, isFalse); + await controller.play(); + expect(controller.value.isPlaying, isTrue); + final FakeVideoEventStream fakeVideoEventStream = + fakeVideoPlayerPlatform.streams[controller.textureId]; + assert(fakeVideoEventStream != null); + + fakeVideoEventStream.eventsChannel + .sendEvent({'event': 'completed'}); + await tester.pumpAndSettle(); + + expect(controller.value.isPlaying, isFalse); + expect(controller.value.position, controller.value.duration); + }); + + testWidgets('buffering status', (WidgetTester tester) async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + expect(controller.value.isBuffering, false); + expect(controller.value.buffered, isEmpty); + final FakeVideoEventStream fakeVideoEventStream = + fakeVideoPlayerPlatform.streams[controller.textureId]; + assert(fakeVideoEventStream != null); + + fakeVideoEventStream.eventsChannel + .sendEvent({'event': 'bufferingStart'}); + await tester.pumpAndSettle(); + expect(controller.value.isBuffering, isTrue); + + const Duration bufferStart = Duration(seconds: 0); + const Duration bufferEnd = Duration(milliseconds: 500); + fakeVideoEventStream.eventsChannel.sendEvent({ + 'event': 'bufferingUpdate', + 'values': >[ + [bufferStart.inMilliseconds, bufferEnd.inMilliseconds] + ], + }); + await tester.pumpAndSettle(); + expect(controller.value.isBuffering, isTrue); + expect(controller.value.buffered.length, 1); + expect(controller.value.buffered[0].toString(), + DurationRange(bufferStart, bufferEnd).toString()); + + fakeVideoEventStream.eventsChannel + .sendEvent({'event': 'bufferingEnd'}); + await tester.pumpAndSettle(); + expect(controller.value.isBuffering, isFalse); + }); + }); + }); + + group('DurationRange', () { + test('uses given values', () { + const Duration start = Duration(seconds: 2); + const Duration end = Duration(seconds: 8); + + final DurationRange range = DurationRange(start, end); + + expect(range.start, start); + expect(range.end, end); + expect(range.toString(), contains('start: $start, end: $end')); + }); + + test('calculates fractions', () { + const Duration start = Duration(seconds: 2); + const Duration end = Duration(seconds: 8); + const Duration total = Duration(seconds: 10); + + final DurationRange range = DurationRange(start, end); + + expect(range.startFraction(total), .2); + expect(range.endFraction(total), .8); + }); + }); + + group('VideoPlayerValue', () { + test('uninitialized()', () { + final VideoPlayerValue uninitialized = VideoPlayerValue.uninitialized(); + + expect(uninitialized.duration, isNull); + expect(uninitialized.position, equals(const Duration(seconds: 0))); + expect(uninitialized.buffered, isEmpty); + expect(uninitialized.isPlaying, isFalse); + expect(uninitialized.isLooping, isFalse); + expect(uninitialized.isBuffering, isFalse); + expect(uninitialized.volume, 1.0); + expect(uninitialized.errorDescription, isNull); + expect(uninitialized.size, isNull); + expect(uninitialized.size, isNull); + expect(uninitialized.initialized, isFalse); + expect(uninitialized.hasError, isFalse); + expect(uninitialized.aspectRatio, 1.0); + }); + + test('erroneous()', () { + const String errorMessage = 'foo'; + final VideoPlayerValue error = VideoPlayerValue.erroneous(errorMessage); + + expect(error.duration, isNull); + expect(error.position, equals(const Duration(seconds: 0))); + expect(error.buffered, isEmpty); + expect(error.isPlaying, isFalse); + expect(error.isLooping, isFalse); + expect(error.isBuffering, isFalse); + expect(error.volume, 1.0); + expect(error.errorDescription, errorMessage); + expect(error.size, isNull); + expect(error.size, isNull); + expect(error.initialized, isFalse); + expect(error.hasError, isTrue); + expect(error.aspectRatio, 1.0); + }); + + test('toString()', () { + const Duration duration = Duration(seconds: 5); + const Size size = Size(400, 300); + const Duration position = Duration(seconds: 1); + final List buffered = [ + DurationRange(const Duration(seconds: 0), const Duration(seconds: 4)) + ]; + const bool isPlaying = true; + const bool isLooping = true; + const bool isBuffering = true; + const double volume = 0.5; + + final VideoPlayerValue value = VideoPlayerValue( + duration: duration, + size: size, + position: position, + buffered: buffered, + isPlaying: isPlaying, + isLooping: isLooping, + isBuffering: isBuffering, + volume: volume); + + expect(value.toString(), + 'VideoPlayerValue(duration: 0:00:05.000000, size: Size(400.0, 300.0), position: 0:00:01.000000, buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], isPlaying: true, isLooping: true, isBuffering: truevolume: 0.5, errorDescription: null)'); + }); + + test('copyWith()', () { + final VideoPlayerValue original = VideoPlayerValue.uninitialized(); + final VideoPlayerValue exactCopy = original.copyWith(); + + expect(exactCopy.toString(), original.toString()); + }); + }); + + test('VideoProgressColors', () { + const Color playedColor = Color.fromRGBO(0, 0, 255, 0.75); + const Color bufferedColor = Color.fromRGBO(0, 255, 0, 0.5); + const Color backgroundColor = Color.fromRGBO(255, 255, 0, 0.25); + + final VideoProgressColors colors = VideoProgressColors( + playedColor: playedColor, + bufferedColor: bufferedColor, + backgroundColor: backgroundColor); + + expect(colors.playedColor, playedColor); + expect(colors.bufferedColor, bufferedColor); + expect(colors.backgroundColor, backgroundColor); + }); +} + +class FakeVideoPlayerPlatform { + FakeVideoPlayerPlatform() { + _channel.setMockMethodCallHandler(onMethodCall); + } + + final MethodChannel _channel = const MethodChannel('flutter.io/videoPlayer'); + + Completer initialized = Completer(); + List calls = []; + List> dataSourceDescriptions = >[]; + final Map streams = {}; + int nextTextureId = 0; + final Map _positions = {}; + + Future onMethodCall(MethodCall call) { + calls.add(call); + switch (call.method) { + case 'init': + initialized.complete(true); + break; + case 'create': + streams[nextTextureId] = FakeVideoEventStream( + nextTextureId, 100, 100, const Duration(seconds: 1)); + final Map dataSource = call.arguments; + dataSourceDescriptions.add(dataSource.cast()); + return Future>.sync(() { + return { + 'textureId': nextTextureId++, + }; + }); + break; + case 'position': + final Duration position = _positions[call.arguments['textureId']] ?? + const Duration(seconds: 0); + return Future.value(position.inMilliseconds); + break; + case 'seekTo': + _positions[call.arguments['textureId']] = + Duration(milliseconds: call.arguments['location']); + break; + case 'dispose': + case 'pause': + case 'play': + case 'setLooping': + case 'setVolume': + break; + default: + throw UnimplementedError( + '${call.method} is not implemented by the FakeVideoPlayerPlatform'); + } + return Future.sync(() {}); + } +} + +class FakeVideoEventStream { + FakeVideoEventStream(this.textureId, this.width, this.height, this.duration) { + eventsChannel = FakeEventsChannel( + 'flutter.io/videoPlayer/videoEvents$textureId', onListen); + } + + int textureId; + int width; + int height; + Duration duration; + FakeEventsChannel eventsChannel; + + void onListen() { + final Map initializedEvent = { + 'event': 'initialized', + 'duration': duration.inMilliseconds, + 'width': width, + 'height': height, + }; + eventsChannel.sendEvent(initializedEvent); + } +} + +class FakeEventsChannel { + FakeEventsChannel(String name, this.onListen) { + eventsMethodChannel = MethodChannel(name); + eventsMethodChannel.setMockMethodCallHandler(onMethodCall); + } + + MethodChannel eventsMethodChannel; + VoidCallback onListen; + + Future onMethodCall(MethodCall call) { + switch (call.method) { + case 'listen': + onListen(); + break; + } + return Future.sync(() {}); + } + + void sendEvent(dynamic event) { + // TODO(jackson): This has been deprecated and should be replaced + // with `ServicesBinding.instance.defaultBinaryMessenger` when it's + // available on all the versions of Flutter that we test. + // ignore: deprecated_member_use + defaultBinaryMessenger.handlePlatformMessage( + eventsMethodChannel.name, + const StandardMethodCodec().encodeSuccessEnvelope(event), + (ByteData data) {}); + } +} diff --git a/packages/video_player/video_player_android.iml b/packages/video_player/video_player/video_player_android.iml similarity index 100% rename from packages/video_player/video_player_android.iml rename to packages/video_player/video_player/video_player_android.iml diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..54d9e6ba4ecb --- /dev/null +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -0,0 +1,7 @@ +## 1.0.1 + +* Return correct platform event type when buffering + +## 1.0.0 + +* Initial release. diff --git a/packages/video_player/video_player_platform_interface/LICENSE b/packages/video_player/video_player_platform_interface/LICENSE new file mode 100644 index 000000000000..c89293372cf3 --- /dev/null +++ b/packages/video_player/video_player_platform_interface/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/video_player/video_player_platform_interface/README.md b/packages/video_player/video_player_platform_interface/README.md new file mode 100644 index 000000000000..02ba8e7166fe --- /dev/null +++ b/packages/video_player/video_player_platform_interface/README.md @@ -0,0 +1,26 @@ +# video_player_platform_interface + +A common platform interface for the [`video_player`][1] plugin. + +This interface allows platform-specific implementations of the `video_player` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `video_player`, extend +[`VideoPlayerPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`VideoPlayerPlatform` by calling +`VideoPlayerPlatform.instance = MyPlatformVideoPlayer()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../video_player +[2]: lib/video_player_platform_interface.dart diff --git a/packages/video_player/video_player_platform_interface/analysis_options.yaml b/packages/video_player/video_player_platform_interface/analysis_options.yaml new file mode 100644 index 000000000000..e1dadb9e2b2a --- /dev/null +++ b/packages/video_player/video_player_platform_interface/analysis_options.yaml @@ -0,0 +1,11 @@ +# This is a temporary file to allow us to land a new set of linter rules in a +# series of manageable patches instead of one gigantic PR. It disables some of +# the new lints that are already failing on this plugin, for this plugin. It +# should be deleted and the failing lints addressed as soon as possible. + +include: ../../../analysis_options.yaml + +analyzer: + errors: + public_member_api_docs: ignore + unawaited_futures: ignore diff --git a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart new file mode 100644 index 000000000000..eb227ce18ec5 --- /dev/null +++ b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart @@ -0,0 +1,178 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import 'video_player_platform_interface.dart'; + +const MethodChannel _channel = MethodChannel('flutter.io/videoPlayer'); + +/// An implementation of [VideoPlayerPlatform] that uses method channels. +class MethodChannelVideoPlayer extends VideoPlayerPlatform { + @override + Future init() { + return _channel.invokeMethod('init'); + } + + @override + Future dispose(int textureId) { + return _channel.invokeMethod( + 'dispose', + {'textureId': textureId}, + ); + } + + @override + Future create(DataSource dataSource) async { + Map dataSourceDescription; + + switch (dataSource.sourceType) { + case DataSourceType.asset: + dataSourceDescription = { + 'asset': dataSource.asset, + 'package': dataSource.package, + }; + break; + case DataSourceType.network: + dataSourceDescription = { + 'uri': dataSource.uri, + 'formatHint': _videoFormatStringMap[dataSource.formatHint] + }; + break; + case DataSourceType.file: + dataSourceDescription = {'uri': dataSource.uri}; + break; + } + + final Map response = + await _channel.invokeMapMethod( + 'create', + dataSourceDescription, + ); + return response['textureId']; + } + + @override + Future setLooping(int textureId, bool looping) { + return _channel.invokeMethod( + 'setLooping', + { + 'textureId': textureId, + 'looping': looping, + }, + ); + } + + @override + Future play(int textureId) { + return _channel.invokeMethod( + 'play', + {'textureId': textureId}, + ); + } + + @override + Future pause(int textureId) { + return _channel.invokeMethod( + 'pause', + {'textureId': textureId}, + ); + } + + @override + Future setVolume(int textureId, double volume) { + return _channel.invokeMethod( + 'setVolume', + { + 'textureId': textureId, + 'volume': volume, + }, + ); + } + + @override + Future seekTo(int textureId, Duration position) { + return _channel.invokeMethod( + 'seekTo', + { + 'textureId': textureId, + 'location': position.inMilliseconds, + }, + ); + } + + @override + Future getPosition(int textureId) async { + return Duration( + milliseconds: await _channel.invokeMethod( + 'position', + {'textureId': textureId}, + ), + ); + } + + @override + Stream videoEventsFor(int textureId) { + return _eventChannelFor(textureId) + .receiveBroadcastStream() + .map((dynamic event) { + final Map map = event; + switch (map['event']) { + case 'initialized': + return VideoEvent( + eventType: VideoEventType.initialized, + duration: Duration(milliseconds: map['duration']), + size: Size(map['width']?.toDouble() ?? 0.0, + map['height']?.toDouble() ?? 0.0), + ); + case 'completed': + return VideoEvent( + eventType: VideoEventType.completed, + ); + case 'bufferingUpdate': + final List values = map['values']; + + return VideoEvent( + buffered: values.map(_toDurationRange).toList(), + eventType: VideoEventType.bufferingUpdate, + ); + case 'bufferingStart': + return VideoEvent(eventType: VideoEventType.bufferingStart); + case 'bufferingEnd': + return VideoEvent(eventType: VideoEventType.bufferingEnd); + default: + return VideoEvent(eventType: VideoEventType.unknown); + } + }); + } + + @override + Widget buildView(int textureId) { + return Texture(textureId: textureId); + } + + EventChannel _eventChannelFor(int textureId) { + return EventChannel('flutter.io/videoPlayer/videoEvents$textureId'); + } + + static const Map _videoFormatStringMap = + { + VideoFormat.ss: 'ss', + VideoFormat.hls: 'hls', + VideoFormat.dash: 'dash', + VideoFormat.other: 'other', + }; + + DurationRange _toDurationRange(dynamic value) { + final List pair = value; + return DurationRange( + Duration(milliseconds: pair[0]), + Duration(milliseconds: pair[1]), + ); + } +} diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart new file mode 100644 index 000000000000..031baf6f218d --- /dev/null +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -0,0 +1,219 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart' show required, visibleForTesting; + +import 'method_channel_video_player.dart'; + +/// The interface that implementations of video_player must implement. +/// +/// Platform implementations should extend this class rather than implement it as `video_player` +/// does not consider newly added methods to be breaking changes. Extending this class +/// (using `extends`) ensures that the subclass will get the default implementation, while +/// platform implementations that `implements` this interface will be broken by newly added +/// [VideoPlayerPlatform] methods. +abstract class VideoPlayerPlatform { + /// Only mock implementations should set this to true. + /// + /// Mockito mocks are implementing this class with `implements` which is forbidden for anything + /// other than mocks (see class docs). This property provides a backdoor for mockito mocks to + /// skip the verification that the class isn't implemented with `implements`. + @visibleForTesting + bool get isMock => false; + + /// The default instance of [VideoPlayerPlatform] to use. + /// + /// Platform-specific plugins should override this with their own + /// platform-specific class that extends [VideoPlayerPlatform] when they + /// register themselves. + /// + /// Defaults to [MethodChannelVideoPlayer]. + static VideoPlayerPlatform _instance = MethodChannelVideoPlayer(); + + static VideoPlayerPlatform get instance => _instance; + + // TODO(amirh): Extract common platform interface logic. + // https://github.com/flutter/flutter/issues/43368 + static set instance(VideoPlayerPlatform instance) { + if (!instance.isMock) { + try { + instance._verifyProvidesDefaultImplementations(); + } on NoSuchMethodError catch (_) { + throw AssertionError( + 'Platform interfaces must not be implemented with `implements`'); + } + } + _instance = instance; + } + + /// Initializes the platform interface and disposes all existing players. + /// + /// This method is called when the plugin is first initialized + /// and on every full restart. + Future init() { + throw UnimplementedError('init() has not been implemented.'); + } + + /// Clears one video. + Future dispose(int textureId) { + throw UnimplementedError('dispose() has not been implemented.'); + } + + /// Creates an instance of a video player and returns its textureId. + Future create(DataSource dataSource) { + throw UnimplementedError('create() has not been implemented.'); + } + + /// Returns a Stream of [VideoEventType]s. + Stream videoEventsFor(int textureId) { + throw UnimplementedError('videoEventsFor() has not been implemented.'); + } + + /// Sets the looping attribute of the video. + Future setLooping(int textureId, bool looping) { + throw UnimplementedError('setLooping() has not been implemented.'); + } + + /// Starts the video playback. + Future play(int textureId) { + throw UnimplementedError('play() has not been implemented.'); + } + + /// Stops the video playback. + Future pause(int textureId) { + throw UnimplementedError('pause() has not been implemented.'); + } + + /// Sets the volume to a range between 0.0 and 1.0. + Future setVolume(int textureId, double volume) { + throw UnimplementedError('setVolume() has not been implemented.'); + } + + /// Sets the video position to a [Duration] from the start. + Future seekTo(int textureId, Duration position) { + throw UnimplementedError('seekTo() has not been implemented.'); + } + + /// Gets the video position as [Duration] from the start. + Future getPosition(int textureId) { + throw UnimplementedError('getPosition() has not been implemented.'); + } + + /// Returns a widget displaying the video with a given textureID. + Widget buildView(int textureId) { + throw UnimplementedError('buildView() has not been implemented.'); + } + + // This method makes sure that VideoPlayer isn't implemented with `implements`. + // + // See class doc for more details on why implementing this class is forbidden. + // + // This private method is called by the instance setter, which fails if the class is + // implemented with `implements`. + void _verifyProvidesDefaultImplementations() {} +} + +class DataSource { + DataSource({ + @required this.sourceType, + this.uri, + this.formatHint, + this.asset, + this.package, + }); + + final DataSourceType sourceType; + final String uri; + final VideoFormat formatHint; + final String asset; + final String package; +} + +enum DataSourceType { + asset, + network, + file, +} + +enum VideoFormat { + dash, + hls, + ss, + other, +} + +class VideoEvent { + VideoEvent({ + @required this.eventType, + this.duration, + this.size, + this.buffered, + }); + + final VideoEventType eventType; + final Duration duration; + final Size size; + final List buffered; + + @override + bool operator ==(Object other) { + return identical(this, other) || + other is VideoEvent && + runtimeType == other.runtimeType && + eventType == other.eventType && + duration == other.duration && + size == other.size && + listEquals(buffered, other.buffered); + } + + @override + int get hashCode => + eventType.hashCode ^ + duration.hashCode ^ + size.hashCode ^ + buffered.hashCode; +} + +enum VideoEventType { + initialized, + completed, + bufferingUpdate, + bufferingStart, + bufferingEnd, + unknown, +} + +class DurationRange { + DurationRange(this.start, this.end); + + final Duration start; + final Duration end; + + double startFraction(Duration duration) { + return start.inMilliseconds / duration.inMilliseconds; + } + + double endFraction(Duration duration) { + return end.inMilliseconds / duration.inMilliseconds; + } + + @override + String toString() => '$runtimeType(start: $start, end: $end)'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DurationRange && + runtimeType == other.runtimeType && + start == other.start && + end == other.end; + + @override + int get hashCode => start.hashCode ^ end.hashCode; +} diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..e36c9583e147 --- /dev/null +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -0,0 +1,21 @@ +name: video_player_platform_interface +description: A common platform interface for the video_player plugin. +author: Flutter Team +homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_platform_interface +# NOTE: We strongly prefer non-breaking changes, even at the expense of a +# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes +version: 1.0.1 + +dependencies: + flutter: + sdk: flutter + meta: ^1.0.5 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^4.1.1 + +environment: + sdk: ">=2.0.0-dev.28.0 <3.0.0" + flutter: ">=1.9.1+hotfix.4 <2.0.0" diff --git a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart new file mode 100644 index 000000000000..02c7bbba22b4 --- /dev/null +++ b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart @@ -0,0 +1,338 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui'; + +import 'package:mockito/mockito.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:video_player_platform_interface/method_channel_video_player.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$VideoPlayerPlatform', () { + test('$MethodChannelVideoPlayer() is the default instance', () { + expect(VideoPlayerPlatform.instance, + isInstanceOf()); + }); + + test('Cannot be implemented with `implements`', () { + expect(() { + VideoPlayerPlatform.instance = ImplementsVideoPlayerPlatform(); + }, throwsA(isInstanceOf())); + }); + + test('Can be mocked with `implements`', () { + final ImplementsVideoPlayerPlatform mock = + ImplementsVideoPlayerPlatform(); + when(mock.isMock).thenReturn(true); + VideoPlayerPlatform.instance = mock; + }); + + test('Can be extended', () { + VideoPlayerPlatform.instance = ExtendsVideoPlayerPlatform(); + }); + }); + + group('$MethodChannelVideoPlayer', () { + const MethodChannel channel = MethodChannel('flutter.io/videoPlayer'); + final List log = []; + final MethodChannelVideoPlayer player = MethodChannelVideoPlayer(); + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + }); + }); + + tearDown(() { + log.clear(); + }); + + test('init', () async { + await player.init(); + expect( + log, + [isMethodCall('init', arguments: null)], + ); + }); + + test('dispose', () async { + await player.dispose(1); + expect( + log, + [ + isMethodCall('dispose', arguments: { + 'textureId': 1, + }) + ], + ); + }); + + test('create with asset', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return {'textureId': 3}; + }); + final int textureId = await player.create(DataSource( + sourceType: DataSourceType.asset, + asset: 'someAsset', + package: 'somePackage', + )); + expect( + log, + [ + isMethodCall('create', arguments: { + 'asset': 'someAsset', + 'package': 'somePackage', + }) + ], + ); + expect(textureId, 3); + }); + + test('create with network', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return {'textureId': 3}; + }); + final int textureId = await player.create(DataSource( + sourceType: DataSourceType.network, + uri: 'someUri', + formatHint: VideoFormat.dash, + )); + expect( + log, + [ + isMethodCall('create', arguments: { + 'uri': 'someUri', + 'formatHint': 'dash' + }) + ], + ); + expect(textureId, 3); + }); + + test('create with file', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return {'textureId': 3}; + }); + final int textureId = await player.create(DataSource( + sourceType: DataSourceType.file, + uri: 'someUri', + )); + expect( + log, + [ + isMethodCall('create', arguments: { + 'uri': 'someUri', + }) + ], + ); + expect(textureId, 3); + }); + + test('setLooping', () async { + await player.setLooping(1, true); + expect( + log, + [ + isMethodCall('setLooping', arguments: { + 'textureId': 1, + 'looping': true, + }) + ], + ); + }); + + test('play', () async { + await player.play(1); + expect( + log, + [ + isMethodCall('play', arguments: { + 'textureId': 1, + }) + ], + ); + }); + + test('pause', () async { + await player.pause(1); + expect( + log, + [ + isMethodCall('pause', arguments: { + 'textureId': 1, + }) + ], + ); + }); + + test('setVolume', () async { + await player.setVolume(1, 0.7); + expect( + log, + [ + isMethodCall('setVolume', arguments: { + 'textureId': 1, + 'volume': 0.7, + }) + ], + ); + }); + + test('seekTo', () async { + await player.seekTo(1, const Duration(milliseconds: 12345)); + expect( + log, + [ + isMethodCall('seekTo', arguments: { + 'textureId': 1, + 'location': 12345, + }) + ], + ); + }); + + test('getPosition', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return 234; + }); + + final Duration position = await player.getPosition(1); + expect( + log, + [ + isMethodCall('position', arguments: { + 'textureId': 1, + }) + ], + ); + expect(position, const Duration(milliseconds: 234)); + }); + + test('videoEventsFor', () async { + // TODO(cbenhagen): This has been deprecated and should be replaced + // with `ServicesBinding.instance.defaultBinaryMessenger` when it's + // available on all the versions of Flutter that we test. + // ignore: deprecated_member_use + defaultBinaryMessenger.setMockMessageHandler( + "flutter.io/videoPlayer/videoEvents123", + (ByteData message) async { + final MethodCall methodCall = + const StandardMethodCodec().decodeMethodCall(message); + if (methodCall.method == 'listen') { + // TODO(cbenhagen): This has been deprecated and should be replaced + // with `ServicesBinding.instance.defaultBinaryMessenger` when it's + // available on all the versions of Flutter that we test. + // ignore: deprecated_member_use + defaultBinaryMessenger.handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'initialized', + 'duration': 98765, + 'width': 1920, + 'height': 1080, + }), + (ByteData data) {}); + + // TODO(cbenhagen): This has been deprecated and should be replaced + // with `ServicesBinding.instance.defaultBinaryMessenger` when it's + // available on all the versions of Flutter that we test. + // ignore: deprecated_member_use + defaultBinaryMessenger.handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'completed', + }), + (ByteData data) {}); + + // TODO(cbenhagen): This has been deprecated and should be replaced + // with `ServicesBinding.instance.defaultBinaryMessenger` when it's + // available on all the versions of Flutter that we test. + // ignore: deprecated_member_use + defaultBinaryMessenger.handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'bufferingUpdate', + 'values': >[ + [0, 1234], + [1235, 4000], + ], + }), + (ByteData data) {}); + + // TODO(cbenhagen): This has been deprecated and should be replaced + // with `ServicesBinding.instance.defaultBinaryMessenger` when it's + // available on all the versions of Flutter that we test. + // ignore: deprecated_member_use + defaultBinaryMessenger.handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'bufferingStart', + }), + (ByteData data) {}); + + // TODO(cbenhagen): This has been deprecated and should be replaced + // with `ServicesBinding.instance.defaultBinaryMessenger` when it's + // available on all the versions of Flutter that we test. + // ignore: deprecated_member_use + defaultBinaryMessenger.handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'bufferingEnd', + }), + (ByteData data) {}); + + return const StandardMethodCodec().encodeSuccessEnvelope(null); + } else if (methodCall.method == 'cancel') { + return const StandardMethodCodec().encodeSuccessEnvelope(null); + } else { + fail('Expected listen or cancel'); + } + }, + ); + expect( + player.videoEventsFor(123), + emitsInOrder([ + VideoEvent( + eventType: VideoEventType.initialized, + duration: const Duration(milliseconds: 98765), + size: const Size(1920, 1080), + ), + VideoEvent(eventType: VideoEventType.completed), + VideoEvent( + eventType: VideoEventType.bufferingUpdate, + buffered: [ + DurationRange( + const Duration(milliseconds: 0), + const Duration(milliseconds: 1234), + ), + DurationRange( + const Duration(milliseconds: 1235), + const Duration(milliseconds: 4000), + ), + ]), + VideoEvent(eventType: VideoEventType.bufferingStart), + VideoEvent(eventType: VideoEventType.bufferingEnd), + ])); + }); + }); +} + +class ImplementsVideoPlayerPlatform extends Mock + implements VideoPlayerPlatform {} + +class ExtendsVideoPlayerPlatform extends VideoPlayerPlatform {} diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index f240d0c8e08c..e577b30f53af 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,16 @@ +## 0.3.16 + +* Add support for async NavigationDelegates. Synchronous NavigationDelegates + should still continue to function without any change in behavior. + +## 0.3.15+3 + +* Re-land support for the v2 Android embedding. This correctly sets the minimum + SDK to the latest stable and avoid any compile errors. *WARNING:* the V2 + embedding itself still requires the current Flutter master channel + (flutter/flutter@1d4d63a) for text input to work properly on all Android + versions. + ## 0.3.15+2 * Remove AndroidX warnings. diff --git a/packages/webview_flutter/analysis_options.yaml b/packages/webview_flutter/analysis_options.yaml new file mode 100644 index 000000000000..d4ccef63f1d1 --- /dev/null +++ b/packages/webview_flutter/analysis_options.yaml @@ -0,0 +1,11 @@ +# This is a temporary file to allow us to land a new set of linter rules in a +# series of manageable patches instead of one gigantic PR. It disables some of +# the new lints that are already failing on this plugin, for this plugin. It +# should be deleted and the failing lints addressed as soon as possible. + +include: ../../analysis_options.yaml + +analyzer: + errors: + public_member_api_docs: ignore + unawaited_futures: ignore diff --git a/packages/webview_flutter/android/build.gradle b/packages/webview_flutter/android/build.gradle index 893badc0e175..2d725840e4c6 100644 --- a/packages/webview_flutter/android/build.gradle +++ b/packages/webview_flutter/android/build.gradle @@ -37,3 +37,28 @@ android { implementation 'androidx.webkit:webkit:1.0.0' } } + +// TODO(mklim): Remove this hack once androidx.lifecycle is included on stable. https://github.com/flutter/flutter/issues/42348 +afterEvaluate { + def containsEmbeddingDependencies = false + for (def configuration : configurations.all) { + for (def dependency : configuration.dependencies) { + if (dependency.group == 'io.flutter' && + dependency.name.startsWith('flutter_embedding') && + dependency.isTransitive()) + { + containsEmbeddingDependencies = true + break + } + } + } + if (!containsEmbeddingDependencies) { + android { + dependencies { + def lifecycle_version = "1.1.1" + compileOnly "android.arch.lifecycle:common-java8:$lifecycle_version" + compileOnly "android.arch.lifecycle:runtime:$lifecycle_version" + } + } + } +} \ No newline at end of file diff --git a/packages/webview_flutter/android/gradle.properties b/packages/webview_flutter/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/webview_flutter/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java index 908f877fb922..86b4fd412a29 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java @@ -15,17 +15,11 @@ import io.flutter.plugin.common.MethodChannel.Result; class FlutterCookieManager implements MethodCallHandler { + private final MethodChannel methodChannel; - private FlutterCookieManager() { - // Do not instantiate. - // This class should only be used in context of a BinaryMessenger. - // Use FlutterCookieManager#registerWith instead. - } - - static void registerWith(BinaryMessenger messenger) { - MethodChannel methodChannel = new MethodChannel(messenger, "plugins.flutter.io/cookie_manager"); - FlutterCookieManager cookieManager = new FlutterCookieManager(); - methodChannel.setMethodCallHandler(cookieManager); + FlutterCookieManager(BinaryMessenger messenger) { + methodChannel = new MethodChannel(messenger, "plugins.flutter.io/cookie_manager"); + methodChannel.setMethodCallHandler(this); } @Override @@ -39,6 +33,10 @@ public void onMethodCall(MethodCall methodCall, Result result) { } } + void dispose() { + methodChannel.setMethodCallHandler(null); + } + private static void clearCookies(final Result result) { CookieManager cookieManager = CookieManager.getInstance(); final boolean hasCookies = cookieManager.hasCookies(); diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java index a7f2db308e15..83a7ed6340f8 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -36,7 +36,7 @@ public class FlutterWebView implements PlatformView, MethodCallHandler { BinaryMessenger messenger, int id, Map params, - final View containerView) { + View containerView) { DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); DisplayManager displayManager = @@ -95,6 +95,26 @@ public void onInputConnectionLocked() { webView.lockInputConnection(); } + // @Override + // This is overriding a method that hasn't rolled into stable Flutter yet. Including the + // annotation would cause compile time failures in versions of Flutter too old to include the new + // method. However leaving it raw like this means that the method will be ignored in old versions + // of Flutter but used as an override anyway wherever it's actually defined. + // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. + public void onFlutterViewAttached(View flutterView) { + webView.setContainerView(flutterView); + } + + // @Override + // This is overriding a method that hasn't rolled into stable Flutter yet. Including the + // annotation would cause compile time failures in versions of Flutter too old to include the new + // method. However leaving it raw like this means that the method will be ignored in old versions + // of Flutter but used as an override anyway wherever it's actually defined. + // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. + public void onFlutterViewDetached() { + webView.setContainerView(null); + } + @Override public void onMethodCall(MethodCall methodCall, Result result) { switch (methodCall.method) { diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java index bdd6abb66282..37ec1c992e26 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java @@ -11,7 +11,6 @@ import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebViewClient; -import androidx.annotation.NonNull; import androidx.webkit.WebViewClientCompat; import io.flutter.plugin.common.MethodChannel; import java.util.HashMap; @@ -124,8 +123,7 @@ public void onUnhandledKeyEvent(WebView view, KeyEvent event) { private WebViewClientCompat internalCreateWebViewClientCompat() { return new WebViewClientCompat() { @Override - public boolean shouldOverrideUrlLoading( - @NonNull WebView view, @NonNull WebResourceRequest request) { + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); } diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java index 9275c380fb56..477eefc3565a 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java @@ -7,6 +7,7 @@ import static android.content.Context.INPUT_METHOD_SERVICE; import android.content.Context; +import android.util.Log; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.webkit.WebView; @@ -22,16 +23,29 @@ *

See also {@link ThreadedInputConnectionProxyAdapterView}. */ final class InputAwareWebView extends WebView { - private final View containerView; - + private static final String TAG = "InputAwareWebView"; private View threadedInputConnectionProxyView; private ThreadedInputConnectionProxyAdapterView proxyAdapterView; + private View containerView; InputAwareWebView(Context context, View containerView) { super(context); this.containerView = containerView; } + void setContainerView(View containerView) { + this.containerView = containerView; + + if (proxyAdapterView == null) { + return; + } + + Log.w(TAG, "The containerView has changed while the proxyAdapterView exists."); + if (containerView != null) { + setInputConnectionTarget(proxyAdapterView); + } + } + /** * Set our proxy adapter view to use its cached input connection instead of creating new ones. * @@ -81,6 +95,12 @@ public boolean checkInputConnectionProxy(final View view) { // This isn't a new ThreadedInputConnectionProxyView. Ignore it. return super.checkInputConnectionProxy(view); } + if (containerView == null) { + Log.e( + TAG, + "Can't create a proxy view because there's no container view. Text input may not work."); + return super.checkInputConnectionProxy(view); + } // We've never seen this before, so we make the assumption that this is WebView's // ThreadedInputConnectionProxyView. We are making the assumption that the only view that could @@ -120,6 +140,10 @@ private void resetInputConnection() { // No need to reset the InputConnection to the default thread if we've never changed it. return; } + if (containerView == null) { + Log.e(TAG, "Can't reset the input connection to the container view because there is none."); + return; + } setInputConnectionTarget(/*targetView=*/ containerView); } @@ -132,6 +156,13 @@ private void resetInputConnection() { * InputConnections should be created on. */ private void setInputConnectionTarget(final View targetView) { + if (containerView == null) { + Log.e( + TAG, + "Can't set the input connection target because there is no containerView to use as a handler."); + return; + } + targetView.requestFocus(); containerView.post( new Runnable() { diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java index 17177541222c..3acbe97c5cec 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java @@ -4,17 +4,71 @@ package io.flutter.plugins.webviewflutter; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.PluginRegistry.Registrar; -/** WebViewFlutterPlugin */ -public class WebViewFlutterPlugin { - /** Plugin registration. */ +/** + * Java platform implementation of the webview_flutter plugin. + * + *

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

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

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

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

Calling this automatically initializes the plugin. However plugins initialized this way + * won't react to changes in activity or context, unlike {@link CameraPlugin}. + */ public static void registerWith(Registrar registrar) { registrar .platformViewRegistry() .registerViewFactory( "plugins.flutter.io/webview", new WebViewFactory(registrar.messenger(), registrar.view())); - FlutterCookieManager.registerWith(registrar.messenger()); + new FlutterCookieManager(registrar.messenger()); + } + + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + BinaryMessenger messenger = binding.getFlutterEngine().getDartExecutor(); + binding + .getFlutterEngine() + .getPlatformViewsController() + .getRegistry() + .registerViewFactory( + "plugins.flutter.io/webview", new WebViewFactory(messenger, /*containerView=*/ null)); + flutterCookieManager = new FlutterCookieManager(messenger); + } + + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) { + if (flutterCookieManager == null) { + return; + } + + flutterCookieManager.dispose(); + flutterCookieManager = null; } } diff --git a/packages/webview_flutter/example/android/app/build.gradle b/packages/webview_flutter/example/android/app/build.gradle index 79a69ac3e4d7..706d501c4060 100644 --- a/packages/webview_flutter/example/android/app/build.gradle +++ b/packages/webview_flutter/example/android/app/build.gradle @@ -56,6 +56,7 @@ flutter { dependencies { testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test:rules:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } diff --git a/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java b/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java new file mode 100644 index 000000000000..fe10c6155e5a --- /dev/null +++ b/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java @@ -0,0 +1,13 @@ +package io.flutter.plugins.webviewflutterexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.e2e.FlutterRunner; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterRunner.class) +public class EmbeddingV1ActivityTest { + @Rule + public ActivityTestRule rule = + new ActivityTestRule<>(EmbeddingV1Activity.class); +} diff --git a/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java b/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java new file mode 100644 index 000000000000..a0bd4fe1a7f5 --- /dev/null +++ b/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java @@ -0,0 +1,11 @@ +package io.flutter.plugins.webviewflutterexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.e2e.FlutterRunner; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterRunner.class) +public class MainActivityTest { + @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); +} diff --git a/packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml index 8fcbcd3908ba..fd570acc8959 100644 --- a/packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml +++ b/packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml @@ -1,39 +1,48 @@ + package="io.flutter.plugins.webviewflutterexample"> - - + + + + + + + + + + + + + - - - - - - - - - - - + + diff --git a/packages/webview_flutter/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1Activity.java b/packages/webview_flutter/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1Activity.java new file mode 100644 index 000000000000..9b868934cc10 --- /dev/null +++ b/packages/webview_flutter/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1Activity.java @@ -0,0 +1,17 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutterexample; + +import android.os.Bundle; +import io.flutter.app.FlutterActivity; +import io.flutter.plugins.GeneratedPluginRegistrant; + +public class EmbeddingV1Activity extends FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + GeneratedPluginRegistrant.registerWith(this); + } +} diff --git a/packages/webview_flutter/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/MainActivity.java b/packages/webview_flutter/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/MainActivity.java index f935d0030483..2f3b7edd3d9f 100644 --- a/packages/webview_flutter/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/MainActivity.java +++ b/packages/webview_flutter/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/MainActivity.java @@ -4,14 +4,22 @@ package io.flutter.plugins.webviewflutterexample; -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugins.webviewflutter.WebViewFlutterPlugin; +/** + * THIS PLUGIN CODE PATH DEPENDS ON A NEWER VERSION OF FLUTTER THAN THE ONE DEFINED IN THE + * PUBSPEC.YAML. Text input will fail on some Android devices unless this is used with at least + * flutter/flutter@1d4d63ace1f801a022ea9ec737bf8c15395588b9. + * + *

Use the V1 embedding as seen in {@link EmbeddingV1Activity} to use this plugin on older + * Flutter versions. + */ public class MainActivity extends FlutterActivity { + // TODO(mklim): Remove this once v2 of GeneratedPluginRegistrant rolls to stable. https://github.com/flutter/flutter/issues/42694 @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); + public void configureFlutterEngine(FlutterEngine flutterEngine) { + flutterEngine.getPlugins().add(new WebViewFlutterPlugin()); } } diff --git a/packages/webview_flutter/example/android/gradle.properties b/packages/webview_flutter/example/android/gradle.properties index 94adc3a3f97a..a6738207fd15 100644 --- a/packages/webview_flutter/example/android/gradle.properties +++ b/packages/webview_flutter/example/android/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true +android.enableR8=true diff --git a/packages/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/example/pubspec.yaml index 3a73afaffa44..ae1d71c1cade 100644 --- a/packages/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/example/pubspec.yaml @@ -1,7 +1,7 @@ name: webview_flutter_example description: Demonstrates how to use the webview_flutter plugin. -version: 1.0.3 +version: 1.0.4 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" @@ -17,6 +17,7 @@ dev_dependencies: sdk: flutter flutter_driver: sdk: flutter + e2e: "^0.2.0" flutter: uses-material-design: true diff --git a/packages/webview_flutter/example/test_driver/webview.dart b/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart similarity index 72% rename from packages/webview_flutter/example/test_driver/webview.dart rename to packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart index e24afd73f557..324ab6140de5 100644 --- a/packages/webview_flutter/example/test_driver/webview.dart +++ b/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart @@ -9,19 +9,17 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import 'package:e2e/e2e.dart'; void main() { - final Completer allTestsCompleter = Completer(); - enableFlutterDriverExtension(handler: (_) => allTestsCompleter.future); - tearDownAll(() => allTestsCompleter.complete(null)); + E2EWidgetsFlutterBinding.ensureInitialized(); - test('initalUrl', () async { + testWidgets('initalUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); - await pumpWidget( + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( @@ -38,10 +36,10 @@ void main() { expect(currentUrl, 'https://flutter.dev/'); }); - test('loadUrl', () async { + testWidgets('loadUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); - await pumpWidget( + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( @@ -61,11 +59,11 @@ void main() { // enable this once https://github.com/flutter/flutter/issues/31510 // is resolved. - test('loadUrl with headers', () async { + testWidgets('loadUrl with headers', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final StreamController pageLoads = StreamController(); - await pumpWidget( + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( @@ -96,12 +94,12 @@ void main() { expect(content.contains('flutter_test_header'), isTrue); }); - test('JavaScriptChannel', () async { + testWidgets('JavaScriptChannel', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final Completer pageLoaded = Completer(); final List messagesReceived = []; - await pumpWidget( + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( @@ -137,7 +135,7 @@ void main() { expect(messagesReceived, equals(['hello'])); }); - test('resize webview', () async { + testWidgets('resize webview', (WidgetTester tester) async { final String resizeTest = ''' Resize test @@ -184,7 +182,7 @@ void main() { javascriptMode: JavascriptMode.unrestricted, ); - await pumpWidget( + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Column( @@ -204,7 +202,7 @@ void main() { expect(resizeCompleter.isCompleted, false); - await pumpWidget( + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Column( @@ -222,11 +220,11 @@ void main() { await resizeCompleter.future; }); - test('set custom userAgent', () async { + testWidgets('set custom userAgent', (WidgetTester tester) async { final Completer controllerCompleter1 = Completer(); final GlobalKey _globalKey = GlobalKey(); - await pumpWidget( + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( @@ -244,7 +242,7 @@ void main() { final String customUserAgent1 = await _getUserAgent(controller1); expect(customUserAgent1, 'Custom_User_Agent1'); // rebuild the WebView with a different user agent. - await pumpWidget( + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( @@ -260,12 +258,13 @@ void main() { expect(customUserAgent2, 'Custom_User_Agent2'); }); - test('use default platform userAgent after webView is rebuilt', () async { + testWidgets('use default platform userAgent after webView is rebuilt', + (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final GlobalKey _globalKey = GlobalKey(); // Build the webView with no user agent to get the default platform user agent. - await pumpWidget( + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( @@ -281,7 +280,7 @@ void main() { final WebViewController controller = await controllerCompleter.future; final String defaultPlatformUserAgent = await _getUserAgent(controller); // rebuild the WebView with a custom user agent. - await pumpWidget( + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( @@ -295,7 +294,7 @@ void main() { final String customUserAgent = await _getUserAgent(controller); expect(customUserAgent, 'Custom_User_Agent'); // rebuilds the WebView with no user agent. - await pumpWidget( + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( @@ -341,12 +340,12 @@ void main() { audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest)); }); - test('Auto media playback', () async { + testWidgets('Auto media playback', (WidgetTester tester) async { Completer controllerCompleter = Completer(); Completer pageLoaded = Completer(); - await pumpWidget( + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( @@ -373,7 +372,7 @@ void main() { pageLoaded = Completer(); // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy - await pumpWidget( + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( @@ -399,13 +398,14 @@ void main() { expect(isPaused, _webviewBool(true)); }); - test('Changes to initialMediaPlaybackPolocy are ignored', () async { + testWidgets('Changes to initialMediaPlaybackPolocy are ignored', + (WidgetTester tester) async { final Completer controllerCompleter = Completer(); Completer pageLoaded = Completer(); final GlobalKey key = GlobalKey(); - await pumpWidget( + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( @@ -430,7 +430,7 @@ void main() { pageLoaded = Completer(); - await pumpWidget( + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( @@ -458,7 +458,7 @@ void main() { }); }); - test('getTitle', () async { + testWidgets('getTitle', (WidgetTester tester) async { final String getTitleTest = ''' Some title @@ -473,7 +473,7 @@ void main() { final Completer controllerCompleter = Completer(); - await pumpWidget( + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( @@ -494,11 +494,123 @@ void main() { final String title = await controller.getTitle(); expect(title, 'Some title'); }); -} -Future pumpWidget(Widget widget) { - runApp(widget); - return WidgetsBinding.instance.endOfFrame; + group('NavigationDelegate', () { + final String blankPage = ""; + final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + + base64Encode(const Utf8Encoder().convert(blankPage)); + + testWidgets('can allow requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.google.com/"'); + + await pageLoads.stream.first; // Wait for the next page load. + final String currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://www.google.com/'); + }); + + testWidgets('can block requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.youtube.com/"'); + + // There should never be any second page load, since our new URL is + // blocked. Still wait for a potential page change for some time in order + // to give the test a chance to fail. + await pageLoads.stream.first + .timeout(const Duration(milliseconds: 500), onTimeout: () => null); + final String currentUrl = await controller.currentUrl(); + expect(currentUrl, isNot(contains('youtube.com'))); + }); + + testWidgets('supports asynchronous decisions', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) async { + NavigationDecision decision = NavigationDecision.prevent; + decision = await Future.delayed( + const Duration(milliseconds: 10), + () => NavigationDecision.navigate); + return decision; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.google.com"'); + + await pageLoads.stream.first; // Wait for second page to load. + final String currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://www.google.com/'); + }); + }); } // JavaScript booleans evaluate to different string values on Android and iOS. diff --git a/packages/webview_flutter/example/test_driver/webview_test.dart b/packages/webview_flutter/example/test_driver/webview_flutter_e2e_test.dart similarity index 72% rename from packages/webview_flutter/example/test_driver/webview_test.dart rename to packages/webview_flutter/example/test_driver/webview_flutter_e2e_test.dart index b0d3305cd652..2e5c27fd402e 100644 --- a/packages/webview_flutter/example/test_driver/webview_test.dart +++ b/packages/webview_flutter/example/test_driver/webview_flutter_e2e_test.dart @@ -3,11 +3,14 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:flutter_driver/flutter_driver.dart'; Future main() async { final FlutterDriver driver = await FlutterDriver.connect(); - await driver.requestData(null, timeout: const Duration(minutes: 1)); + final String result = + await driver.requestData(null, timeout: const Duration(minutes: 1)); driver.close(); + exit(result == 'pass' ? 0 : 1); } diff --git a/packages/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/lib/platform_interface.dart index 7e82bae91138..cf6a2fd933f7 100644 --- a/packages/webview_flutter/lib/platform_interface.dart +++ b/packages/webview_flutter/lib/platform_interface.dart @@ -21,7 +21,7 @@ abstract class WebViewPlatformCallbacksHandler { /// Invoked by [WebViewPlatformController] when a navigation request is pending. /// /// If true is returned the navigation is allowed, otherwise it is blocked. - bool onNavigationRequest({String url, bool isForMainFrame}); + FutureOr onNavigationRequest({String url, bool isForMainFrame}); /// Invoked by [WebViewPlatformController] when a page has finished loading. void onPageFinished(String url); diff --git a/packages/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/lib/src/webview_method_channel.dart index c2949cc77a2a..2bb7470a1145 100644 --- a/packages/webview_flutter/lib/src/webview_method_channel.dart +++ b/packages/webview_flutter/lib/src/webview_method_channel.dart @@ -31,7 +31,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { _platformCallbacksHandler.onJavaScriptChannelMessage(channel, message); return true; case 'navigationRequest': - return _platformCallbacksHandler.onNavigationRequest( + return await _platformCallbacksHandler.onNavigationRequest( url: call.arguments['url'], isForMainFrame: call.arguments['isForMainFrame'], ); diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index cd5ca46701d7..11541d322dfb 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -67,7 +67,8 @@ enum NavigationDecision { /// `navigation` should be handled. /// /// See also: [WebView.navigationDelegate]. -typedef NavigationDecision NavigationDelegate(NavigationRequest navigation); +typedef FutureOr NavigationDelegate( + NavigationRequest navigation); /// Signature for when a [WebView] has finished loading a page. typedef void PageFinishedCallback(String url); @@ -439,11 +440,12 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { } @override - bool onNavigationRequest({String url, bool isForMainFrame}) { + FutureOr onNavigationRequest({String url, bool isForMainFrame}) async { final NavigationRequest request = NavigationRequest._(url: url, isForMainFrame: isForMainFrame); final bool allowNavigation = _widget.navigationDelegate == null || - _widget.navigationDelegate(request) == NavigationDecision.navigate; + await _widget.navigationDelegate(request) == + NavigationDecision.navigate; return allowNavigation; } diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index 5be58f06e72d..4883f049f759 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml @@ -1,12 +1,12 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. -version: 0.3.15+2 +version: 0.3.16 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" - flutter: ">=1.5.0 <2.0.0" + flutter: ">=1.9.1+hotfix.5 <2.0.0" dependencies: flutter: diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 000000000000..23fb3db51bc0 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,7 @@ +# This only exists so that we can include pedantic in the top level +# analysis_options.yaml. Each plugin should be published from its own +# subdirectory underneath packages/. +name: flutter_plugins +dev_dependencies: + pedantic: ^1.8.0 +publish_to: none diff --git a/script/build_all_plugins_app.sh b/script/build_all_plugins_app.sh index 5ba4b6e5ce02..1028ee607386 100755 --- a/script/build_all_plugins_app.sh +++ b/script/build_all_plugins_app.sh @@ -10,7 +10,16 @@ readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" source "$SCRIPT_DIR/common.sh" check_changed_packages > /dev/null -(cd "$REPO_DIR" && pub global run flutter_plugin_tools all-plugins-app --exclude instrumentation_adapter,url_launcher_platform_interface) +readonly EXCLUDED_PLUGINS_LIST=( + "instrumentation_adapter" + "url_launcher_platform_interface" + "google_sign_in_platform_interface" + "video_player_platform_interface" +) +# Comma-separated string of the list above +readonly EXCLUDED=$(IFS=, ; echo "${EXCLUDED_PLUGINS_LIST[*]}") + +(cd "$REPO_DIR" && pub global run flutter_plugin_tools all-plugins-app --exclude $EXCLUDED) function error() { echo "$@" 1>&2 diff --git a/script/check_publish.sh b/script/check_publish.sh index 72739410f17c..8c35642316ef 100755 --- a/script/check_publish.sh +++ b/script/check_publish.sh @@ -17,7 +17,7 @@ function check_publish() { echo "Checking that $package_name can be published." if [[ $(cd "$dir" && cat pubspec.yaml | grep -E "^publish_to: none") ]]; then echo "Package $package_name is marked as unpublishable. Skipping." - elif (cd "$dir" && pub publish --dry-run > /dev/null); then + elif (cd "$dir" && flutter pub publish -- --dry-run > /dev/null); then echo "Package $package_name is able to be published." else error "Unable to publish $package_name" diff --git a/script/lint_darwin_plugins.sh b/script/lint_darwin_plugins.sh index 94041c728191..7e749707128c 100755 --- a/script/lint_darwin_plugins.sh +++ b/script/lint_darwin_plugins.sh @@ -16,6 +16,7 @@ function lint_package() { # These podspecs are temporary multi-platform adoption dummy files. local skipped_podspecs=( "url_launcher_web.podspec" + "google_sign_in_web.podspec" ) # TODO: These packages have analyzer warnings. Remove plugins from this list as issues are fixed. @@ -70,16 +71,8 @@ function lint_packages() { return fi - # TODO: These packages have linter errors. Remove plugins from this list as linter issues are fixed. - local skipped_packages=( - 'google_maps_flutter' - ) - local failure_count=0 - for package_name in "$@"; do - if [[ "${skipped_packages[*]}" =~ "${package_name}" ]]; then - continue - fi + for package_name in "$@"; do lint_package "${package_name}" failure_count+="$?" done