diff --git a/.ci.yaml b/.ci.yaml index c00fcf0b7865..a8fa7a5a1577 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -11,9 +11,6 @@ enabled_branches: platform_properties: linux: properties: - caches: >- - [ - ] dependencies: > [ {"dependency": "curl"} @@ -22,11 +19,6 @@ platform_properties: os: Linux windows: properties: - caches: >- - [ - {"name": "vsbuild", "path": "vsbuild"}, - {"name": "pub_cache", "path": ".pub-cache"} - ] dependencies: > [ {"dependency": "certs"} @@ -85,19 +77,6 @@ targets: {"dependency": "vs_build"} ] - - name: Windows uwp-platform_tests master - recipe: plugins/plugins - timeout: 30 - properties: - add_recipes_cq: "true" - target_file: uwp_build_and_platform_tests.yaml - channel: master - version_file: flutter_master.version - dependencies: > - [ - {"dependency": "vs_build"} - ] - - name: Windows plugin_tools_tests recipe: plugins/plugins timeout: 30 diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 73efa3103922..59d4064f0a91 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,7 +1,7 @@ # The Flutter version is not important here, since the CI scripts update Flutter # before running. What matters is that the base image is pinned to minimize # unintended changes when modifying this file. -FROM cirrusci/flutter:2.8.0 +FROM cirrusci/flutter@sha256:505fe8bce2896c75b4df9ccf500b1604155bf932af7465ffcc66fcae8612f82f RUN apt-get update -y diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 09a4cea57f76..a750a5ec244f 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -509ddfda5c1bda7c01cf51fe24607da19423d5f9 +7965ee25dd632d907f69e5ea6f4f81f35c0cf6a7 diff --git a/.ci/scripts/build_examples_uwp.sh b/.ci/scripts/build_examples_uwp.sh deleted file mode 100644 index 04b8256891bd..000000000000 --- a/.ci/scripts/build_examples_uwp.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -# Copyright 2013 The Flutter Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -dart ./script/tool/bin/flutter_plugin_tools.dart build-examples --winuwp \ - --packages-for-branch --log-timing diff --git a/.ci/targets/uwp_build_and_platform_tests.yaml b/.ci/targets/uwp_build_and_platform_tests.yaml deleted file mode 100644 index a7f070776ff1..000000000000 --- a/.ci/targets/uwp_build_and_platform_tests.yaml +++ /dev/null @@ -1,5 +0,0 @@ -tasks: - - name: prepare tool - script: .ci/scripts/prepare_tool.sh - - name: build examples (UWP) - script: .ci/scripts/build_examples_uwp.sh diff --git a/.cirrus.yml b/.cirrus.yml index 8f6a6f7f6ebe..c26118d1443f 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,8 +1,9 @@ gcp_credentials: ENCRYPTED[!2c88dee9c9d9805b214c9f7ad8f3bc8fae936cdb0f881d562101151c408c7e024a41222677d5831df90c60d2dd6cd80a!] # Don't run on release tags since it creates O(n^2) tasks where n is the -# number of plugins -only_if: $CIRRUS_TAG == '' +# number of plugins. +# Don't run on 'master' since it's a mirror of 'main'. +only_if: $CIRRUS_TAG == '' && $CIRRUS_BRANCH != 'master' env: CHANNEL: "master" # Default to master when not explicitly set by a task. PLUGIN_TOOL: "./script/tool/bin/flutter_plugin_tools.dart" @@ -101,6 +102,13 @@ task: always: format_script: ./script/tool_runner.sh format --fail-on-change pubspec_script: ./script/tool_runner.sh pubspec-check + readme_script: + - ./script/tool_runner.sh readme-check + # Re-run with --require-excerpts, skipping packages that still need + # to be converted. Once https://github.com/flutter/flutter/issues/102679 + # has been fixed, this can be removed and there can just be a single + # run with --require-excerpts and no exclusions. + - ./script/tool_runner.sh readme-check --require-excerpts --exclude=script/configs/temp_exclude_excerpt.yaml license_script: dart $PLUGIN_TOOL license-check - name: federated_safety # This check is only meaningful for PRs, as it validates changes @@ -154,6 +162,10 @@ task: analyze_script: - ./script/tool_runner.sh analyze --skip-if-not-supporting-flutter-version="$CHANNEL" --custom-analysis=script/configs/custom_analysis.yaml - echo "If this test fails, the minumum Flutter version should be updated" + - name: readme_excerpts + env: + CIRRUS_CLONE_SUBMODULES: true + script: ./script/tool_runner.sh update-excerpts --fail-on-change ### Web tasks ### - name: web-build_all_plugins env: diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..676ac1da5930 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,289 @@ +version: 2 +updates: + - package-ecosystem: "gradle" + directory: "/packages/camera/camera/android" + commit-message: + prefix: "[camera]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/camera/camera/example/android/app" + commit-message: + prefix: "[camera]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/espresso/android" + commit-message: + prefix: "[espresso]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/espresso/example/android/app" + commit-message: + prefix: "[espresso]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/flutter_plugin_android_lifecycle/android" + commit-message: + prefix: "[lifecycle]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/flutter_plugin_android_lifecycle/example/android/app" + commit-message: + prefix: "[lifecycle]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/google_maps_flutter/google_maps_flutter/android" + commit-message: + prefix: "[google_maps]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/google_maps_flutter/google_maps_flutter/example/android/app" + commit-message: + prefix: "[google_maps]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/google_sign_in/google_sign_in/example/android/app" + commit-message: + prefix: "[sign_in]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/google_sign_in/google_sign_in_android/android" + commit-message: + prefix: "[sign_in]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/google_sign_in/google_sign_in_android/example/android/app" + commit-message: + prefix: "[sign_in]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/in_app_purchase/in_app_purchase_android/android" + commit-message: + prefix: "[in_app_pur]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/in_app_purchase/in_app_purchase_android/example/android/app" + commit-message: + prefix: "[in_app_pur]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/in_app_purchase/in_app_purchase/example/android/app" + commit-message: + prefix: "[in_app_pur]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/image_picker/image_picker/example/android/app" + commit-message: + prefix: "[image_picker]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/image_picker/image_picker_android/android" + commit-message: + prefix: "[image_picker]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/image_picker/image_picker_android/example/android/app" + commit-message: + prefix: "[image_picker]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/local_auth/local_auth_android/android" + commit-message: + prefix: "[local_auth]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/local_auth/local_auth_android/example/android/app" + commit-message: + prefix: "[local_auth]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/local_auth/local_auth/example/android/app" + commit-message: + prefix: "[local_auth]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/path_provider/path_provider/example/android/app" + commit-message: + prefix: "[path_provider]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/path_provider/path_provider_android/android" + commit-message: + prefix: "[path_provider]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/path_provider/path_provider_android/example/android/app" + commit-message: + prefix: "[path_provider]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/quick_actions/quick_actions_android/android" + commit-message: + prefix: "[quick_actions]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/quick_actions/quick_actions_android/example/android/app" + commit-message: + prefix: "[quick_actions]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/quick_actions/quick_actions/example/android/app" + commit-message: + prefix: "[quick_actions]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/shared_preferences/shared_preferences/example/android/app" + commit-message: + prefix: "[shared_pref]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/shared_preferences/shared_preferences_android/example/android/app" + commit-message: + prefix: "[shared_pref]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/url_launcher/url_launcher_android/android" + commit-message: + prefix: "[url_launcher]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/url_launcher/url_launcher_android/example/android/app" + commit-message: + prefix: "[url_launcher]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/url_launcher/url_launcher/example/android/app" + commit-message: + prefix: "[url_launcher]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/video_player/video_player_android/android" + commit-message: + prefix: "[video_player]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/video_player/video_player_android/example/android/app" + commit-message: + prefix: "[video_player]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/webview_flutter/webview_flutter/example/android/app" + commit-message: + prefix: "[webview]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/webview_flutter/webview_flutter_android/android" + commit-message: + prefix: "[webview]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/webview_flutter/webview_flutter_android/example/android" + commit-message: + prefix: "[webview]" + schedule: + interval: "daily" + open-pull-requests-limit: 10 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000000..1d3bb5da1bfb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "site-shared"] + path = site-shared + url = https://github.com/dart-lang/site-shared diff --git a/CODEOWNERS b/CODEOWNERS index 88ba1f575a4c..bc3e392d1766 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -5,8 +5,40 @@ # reviewed by someone else. # Plugin-level rules. -packages/webview_flutter/** @bparrishMines +packages/path_provider/** @gaaclarke +packages/shared_preferences/** @gaaclarke +packages/video_player/** @gaaclarke +packages/webview_flutter/** @bparrishMines # Sub-package-level rules. These should stay last, since the last matching # entry takes precedence. -packages/**/*_web/** @ditman + +# - Web +packages/**/*_web/** @ditman + +# - Android +packages/camera/camera/android/** @camsim99 +packages/google_maps_flutter/google_maps_flutter/android/** @GaryQian +packages/google_sign_in/google_sign_in_android/** @camsim99 +packages/image_picker/image_picker_android/** @GaryQian +packages/in_app_purchase/in_app_purchase_android/** @blasten +packages/local_auth/local_auth_android/** @blasten +packages/path_provider/path_provider_android/** @camsim99 +packages/quick_actions/quick_actions_android/** @camsim99 +packages/url_launcher/url_launcher_android/** @GaryQian +packages/video_player/video_player_android/** @blasten + +# - iOS +packages/camera/camera/ios/** @hellohuanlin +packages/google_maps_flutter/google_maps_flutter/ios/** @cyanglaz +packages/google_sign_in/google_sign_in_ios/** @jmagman +packages/image_picker/image_picker_ios/** @cyanglaz +packages/in_app_purchase/in_app_purchase_storekit/** @cyanglaz +packages/ios_platform_images/ios/** @jmagman +packages/local_auth/local_auth_ios/** @hellohuanlin +packages/path_provider/path_provider_ios/** @jmagman +packages/quick_actions/quick_actions_ios/** @hellohuanlin +packages/shared_preferences/shared_preferences_ios/** @cyanglaz +packages/url_launcher/url_launcher_ios/** @jmagman +packages/video_player/video_player_avfoundation/** @hellohuanlin +packages/webview_flutter/webview_flutter_wkwebview/** @cyanglaz diff --git a/analysis_options.yaml b/analysis_options.yaml index 60d2c33601eb..f6177cd9939a 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -49,9 +49,6 @@ analyzer: # Allow null checks for as long as mixed mode is officially supported. unnecessary_null_comparison: false always_require_non_null_named_parameters: false # not needed with nnbd - # TODO(https://github.com/flutter/flutter/issues/74381): - # Clean up existing unnecessary imports, and remove line to ignore. - unnecessary_import: ignore exclude: # Ignore generated files - '**/*.g.dart' diff --git a/analysis_options_legacy.yaml b/analysis_options_legacy.yaml index b2a343f220c8..da3c18071650 100644 --- a/analysis_options_legacy.yaml +++ b/analysis_options_legacy.yaml @@ -8,9 +8,6 @@ analyzer: - '**/*.pigeon.dart' # Pigeon generated file errors: always_require_non_null_named_parameters: false # not needed with nnbd - # TODO(https://github.com/flutter/flutter/issues/74381): - # Clean up existing unnecessary imports, and remove line to ignore. - unnecessary_import: ignore unnecessary_null_comparison: false # Turned as long as nnbd mix-mode is supported. linter: rules: diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 35a958e026ff..4d7e9bbeb218 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,27 @@ +## NEXT + +* Removes unnecessary imports. + +## 0.9.4+21 + +* Fixes README code samples. + +## 0.9.4+20 + +* Fixes an issue with the orientation of videos recorded in landscape on Android. + +## 0.9.4+19 + +* Migrate deprecated Scaffold SnackBar methods to ScaffoldMessenger. + +## 0.9.4+18 + +* Fixes a crash in iOS when streaming on low-performance devices. + +## 0.9.4+17 + +* Removes obsolete information from README, and adds OS support table. + ## 0.9.4+16 * Fixes a bug resulting in a `CameraAccessException` that prevents image diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md index 6cca254f8690..97b16d20f48a 100644 --- a/packages/camera/camera/README.md +++ b/packages/camera/camera/README.md @@ -1,10 +1,14 @@ # Camera Plugin + + [![pub package](https://img.shields.io/pub/v/camera.svg)](https://pub.dev/packages/camera) A Flutter plugin for iOS, Android and Web allowing access to the device cameras. -*Note*: This plugin is still under development, and some APIs might not be available yet. We are working on a refactor which can be followed here: [issue](https://github.com/flutter/flutter/issues/31225) +| | Android | iOS | Web | +|----------------|---------|----------|------------------------| +| **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] | ## Features @@ -19,8 +23,9 @@ First, add `camera` as a [dependency in your pubspec.yaml file](https://flutter. ### iOS -The camera plugin functionality works on iOS 10.0 or higher. If compiling for any version lower than 10.0, -make sure to programmatically check the version of iOS running on the device before using any camera plugin features. +\* The camera plugin compiles for any version of iOS, but its functionality +requires iOS 10 or higher. If compiling for iOS 9, make sure to programmatically +check the version of iOS running on the device before using any camera plugin features. The [device_info_plus](https://pub.dev/packages/device_info_plus) plugin, for example, can be used to check the iOS version. Add two rows to the `ios/Runner/Info.plist`: @@ -28,20 +33,20 @@ Add two rows to the `ios/Runner/Info.plist`: * one with the key `Privacy - Camera Usage Description` and a usage description. * and one with the key `Privacy - Microphone Usage Description` and a usage description. -Or in text format add the key: +If editing `Info.plist` as text, add: ```xml NSCameraUsageDescription -Can I use the camera please? +your usage description here NSMicrophoneUsageDescription -Can I use the mic please? +your usage description here ``` ### Android Change the minimum Android sdk version to 21 (or higher) in your `android/app/build.gradle` file. -``` +```groovy minSdkVersion 21 ``` @@ -56,33 +61,35 @@ For web integration details, see the As of version [0.5.0](https://github.com/flutter/plugins/blob/master/packages/camera/CHANGELOG.md#050) of the camera plugin, lifecycle changes are no longer handled by the plugin. This means developers are now responsible to control camera resources when the lifecycle state is updated. Failure to do so might lead to unexpected behavior (for example as described in issue [#39109](https://github.com/flutter/flutter/issues/39109)). Handling lifecycle changes can be done by overriding the `didChangeAppLifecycleState` method like so: + ```dart - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - // App state changed before we got the chance to initialize. - if (controller == null || !controller.value.isInitialized) { - return; - } - if (state == AppLifecycleState.inactive) { - controller?.dispose(); - } else if (state == AppLifecycleState.resumed) { - if (controller != null) { - onNewCameraSelected(controller.description); - } - } +@override +void didChangeAppLifecycleState(AppLifecycleState state) { + final CameraController? cameraController = controller; + + // App state changed before we got the chance to initialize. + if (cameraController == null || !cameraController.value.isInitialized) { + return; } + + if (state == AppLifecycleState.inactive) { + cameraController.dispose(); + } else if (state == AppLifecycleState.resumed) { + onNewCameraSelected(cameraController.description); + } +} ``` ### Example Here is a small example flutter app displaying a full screen camera preview. + ```dart -import 'dart:async'; -import 'package:flutter/material.dart'; import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; -List cameras; +late List cameras; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -97,7 +104,7 @@ class CameraApp extends StatefulWidget { } class _CameraAppState extends State { - CameraController controller; + late CameraController controller; @override void initState() { @@ -113,7 +120,7 @@ class _CameraAppState extends State { @override void dispose() { - controller?.dispose(); + controller.dispose(); super.dispose(); } @@ -127,11 +134,8 @@ class _CameraAppState extends State { ); } } - ``` For a more elaborate usage example see [here](https://github.com/flutter/plugins/tree/main/packages/camera/camera/example). -*Note*: This plugin is still under development, and some APIs might not be available yet. -[Feedback welcome](https://github.com/flutter/flutter/issues) and -[Pull Requests](https://github.com/flutter/plugins/pulls) are most welcome! +[1]: https://pub.dev/packages/camera_web#limitations-on-the-web-platform diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java index dd1e489e6225..ec6fa13dbd1d 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java @@ -142,26 +142,32 @@ public int getPhotoOrientation(PlatformChannel.DeviceOrientation orientation) { } /** - * Returns the device's video orientation in degrees based on the sensor orientation and the last - * known UI orientation. + * Returns the device's video orientation in clockwise degrees based on the sensor orientation and + * the last known UI orientation. * *

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

Returns one of 0, 90, 180 or 270. * + *

More details can be found in the official Android documentation: + * https://developer.android.com/reference/android/media/MediaRecorder#setOrientationHint(int) + * + *

See also: + * https://developer.android.com/training/camera2/camera-preview-large-screens#orientation_calculation + * * @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted * into degrees. - * @return The device's video orientation in degrees. + * @return The device's video orientation in clockwise degrees. */ public int getVideoOrientation(PlatformChannel.DeviceOrientation orientation) { int angle = 0; @@ -179,10 +185,10 @@ public int getVideoOrientation(PlatformChannel.DeviceOrientation orientation) { angle = 180; break; case LANDSCAPE_LEFT: - angle = 90; + angle = 270; break; case LANDSCAPE_RIGHT: - angle = 270; + angle = 90; break; } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java index 82449a10188a..3762006f46d4 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java @@ -62,9 +62,9 @@ public void getVideoOrientation_whenNaturalScreenOrientationEqualsPortraitUp() { deviceOrientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); assertEquals(0, degreesPortraitUp); - assertEquals(90, degreesLandscapeLeft); + assertEquals(270, degreesLandscapeLeft); assertEquals(180, degreesPortraitDown); - assertEquals(270, degreesLandscapeRight); + assertEquals(90, degreesLandscapeRight); } @Test @@ -81,18 +81,30 @@ public void getVideoOrientation_whenNaturalScreenOrientationEqualsLandscapeLeft( orientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); assertEquals(90, degreesPortraitUp); - assertEquals(180, degreesLandscapeLeft); + assertEquals(0, degreesLandscapeLeft); assertEquals(270, degreesPortraitDown); - assertEquals(0, degreesLandscapeRight); + assertEquals(180, degreesLandscapeRight); } @Test - public void getVideoOrientation_shouldFallbackToSensorOrientationWhenOrientationIsNull() { - setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + public void getVideoOrientation_fallbackToPortraitSensorOrientationWhenOrientationIsNull() { + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); int degrees = deviceOrientationManager.getVideoOrientation(null); - assertEquals(90, degrees); + assertEquals(0, degrees); + } + + @Test + public void getVideoOrientation_fallbackToLandscapeSensorOrientationWhenOrientationIsNull() { + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + DeviceOrientationManager orientationManager = + DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 90); + + int degrees = orientationManager.getVideoOrientation(null); + + assertEquals(0, degrees); } @Test diff --git a/packages/camera/camera/example/build.excerpt.yaml b/packages/camera/camera/example/build.excerpt.yaml new file mode 100644 index 000000000000..e317efa11cb3 --- /dev/null +++ b/packages/camera/camera/example/build.excerpt.yaml @@ -0,0 +1,15 @@ +targets: + $default: + sources: + include: + - lib/** + # Some default includes that aren't really used here but will prevent + # false-negative warnings: + - $package$ + - lib/$lib$ + exclude: + - '**/.*/**' + - '**/build/**' + builders: + code_excerpter|code_excerpter: + enabled: true diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index 6c171505a8ca..37f56d0ed52e 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 25C3919135C3D981E6F800D0 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1944D8072499F3B5E7653D44 /* libPods-RunnerTests.a */; }; 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 788A065A27B0E02900533D74 /* StreamingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 788A065927B0E02900533D74 /* StreamingTest.m */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; @@ -70,6 +71,7 @@ 1944D8072499F3B5E7653D44 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 59848A7CA98C1FADF8840207 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 788A065927B0E02900533D74 /* StreamingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StreamingTest.m; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -142,6 +144,7 @@ F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */, E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */, E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */, + 788A065927B0E02900533D74 /* StreamingTest.m */, ); path = RunnerTests; sourceTree = ""; @@ -416,6 +419,7 @@ E0CDBAC227CD9729002561D9 /* CameraTestUtils.m in Sources */, 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */, + 788A065A27B0E02900533D74 /* StreamingTest.m in Sources */, E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */, E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */, E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */, diff --git a/packages/camera/camera/example/ios/RunnerTests/StreamingTest.m b/packages/camera/camera/example/ios/RunnerTests/StreamingTest.m new file mode 100644 index 000000000000..1843cce12152 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/StreamingTest.m @@ -0,0 +1,85 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import camera; +@import camera.Test; +@import XCTest; +@import AVFoundation; +#import +#import "CameraTestUtils.h" + +@interface StreamingTests : XCTestCase +@property(readonly, nonatomic) FLTCam *camera; +@property(readonly, nonatomic) CMSampleBufferRef sampleBuffer; +@end + +@implementation StreamingTests + +- (void)setUp { + dispatch_queue_t captureSessionQueue = dispatch_queue_create("testing", NULL); + _camera = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue); + _sampleBuffer = FLTCreateTestSampleBuffer(); +} + +- (void)tearDown { + CFRelease(_sampleBuffer); +} + +- (void)testExceedMaxStreamingPendingFramesCount { + XCTestExpectation *streamingExpectation = [self + expectationWithDescription:@"Must not call handler over maxStreamingPendingFramesCount"]; + + id handlerMock = OCMClassMock([FLTImageStreamHandler class]); + OCMStub([handlerMock eventSink]).andReturn(^(id event) { + [streamingExpectation fulfill]; + }); + + id messenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + [_camera startImageStreamWithMessenger:messenger imageStreamHandler:handlerMock]; + + XCTKVOExpectation *expectation = [[XCTKVOExpectation alloc] initWithKeyPath:@"isStreamingImages" + object:_camera + expectedValue:@YES]; + XCTWaiterResult result = [XCTWaiter waitForExpectations:@[ expectation ] timeout:1]; + XCTAssertEqual(result, XCTWaiterResultCompleted); + + streamingExpectation.expectedFulfillmentCount = 4; + for (int i = 0; i < 10; i++) { + [_camera captureOutput:nil didOutputSampleBuffer:self.sampleBuffer fromConnection:nil]; + } + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +- (void)testReceivedImageStreamData { + XCTestExpectation *streamingExpectation = + [self expectationWithDescription: + @"Must be able to call the handler again when receivedImageStreamData is called"]; + + id handlerMock = OCMClassMock([FLTImageStreamHandler class]); + OCMStub([handlerMock eventSink]).andReturn(^(id event) { + [streamingExpectation fulfill]; + }); + + id messenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + [_camera startImageStreamWithMessenger:messenger imageStreamHandler:handlerMock]; + + XCTKVOExpectation *expectation = [[XCTKVOExpectation alloc] initWithKeyPath:@"isStreamingImages" + object:_camera + expectedValue:@YES]; + XCTWaiterResult result = [XCTWaiter waitForExpectations:@[ expectation ] timeout:1]; + XCTAssertEqual(result, XCTWaiterResultCompleted); + + streamingExpectation.expectedFulfillmentCount = 5; + for (int i = 0; i < 10; i++) { + [_camera captureOutput:nil didOutputSampleBuffer:self.sampleBuffer fromConnection:nil]; + } + + [_camera receivedImageStreamData]; + [_camera captureOutput:nil didOutputSampleBuffer:self.sampleBuffer fromConnection:nil]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +@end diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index d47edfed69e2..aabbe249313d 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -10,6 +10,7 @@ import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:video_player/video_player.dart'; class CameraExampleHome extends StatefulWidget { @@ -105,6 +106,7 @@ class _CameraExampleHomeState extends State super.dispose(); } + // #docregion AppLifecycle @override void didChangeAppLifecycleState(AppLifecycleState state) { final CameraController? cameraController = controller; @@ -120,13 +122,11 @@ class _CameraExampleHomeState extends State onNewCameraSelected(cameraController.description); } } - - final GlobalKey _scaffoldKey = GlobalKey(); + // #enddocregion AppLifecycle @override Widget build(BuildContext context) { return Scaffold( - key: _scaffoldKey, appBar: AppBar( title: const Text('Camera example'), ), @@ -583,7 +583,10 @@ class _CameraExampleHomeState extends State }; if (cameras.isEmpty) { - return const Text('No camera found'); + _ambiguate(SchedulerBinding.instance)?.addPostFrameCallback((_) async { + showInSnackBar('No camera found.'); + }); + return const Text('None'); } else { for (final CameraDescription cameraDescription in cameras) { toggles.add( @@ -609,8 +612,8 @@ class _CameraExampleHomeState extends State String timestamp() => DateTime.now().millisecondsSinceEpoch.toString(); void showInSnackBar(String message) { - // ignore: deprecated_member_use - _scaffoldKey.currentState?.showSnackBar(SnackBar(content: Text(message))); + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(message))); } void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { diff --git a/packages/camera/camera/example/lib/readme_full_example.dart b/packages/camera/camera/example/lib/readme_full_example.dart new file mode 100644 index 000000000000..b25e637a0c95 --- /dev/null +++ b/packages/camera/camera/example/lib/readme_full_example.dart @@ -0,0 +1,56 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +// #docregion FullAppExample +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; + +late List cameras; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + cameras = await availableCameras(); + runApp(CameraApp()); +} + +class CameraApp extends StatefulWidget { + @override + _CameraAppState createState() => _CameraAppState(); +} + +class _CameraAppState extends State { + late CameraController controller; + + @override + void initState() { + super.initState(); + controller = CameraController(cameras[0], ResolutionPreset.max); + controller.initialize().then((_) { + if (!mounted) { + return; + } + setState(() {}); + }); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (!controller.value.isInitialized) { + return Container(); + } + return MaterialApp( + home: CameraPreview(controller), + ); + } +} +// #enddocregion FullAppExample diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index 1700074f1f88..af4d078ff836 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: video_player: ^2.1.4 dev_dependencies: + build_runner: ^2.1.10 flutter_driver: sdk: flutter flutter_test: diff --git a/packages/camera/camera/example/test/main_test.dart b/packages/camera/camera/example/test/main_test.dart new file mode 100644 index 000000000000..9a5fcdf2d5ea --- /dev/null +++ b/packages/camera/camera/example/test/main_test.dart @@ -0,0 +1,16 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_example/main.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Test snackbar', (WidgetTester tester) async { + WidgetsFlutterBinding.ensureInitialized(); + await tester.pumpWidget(CameraApp()); + await tester.pumpAndSettle(); + expect(find.byType(SnackBar), findsOneWidget); + }); +} diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 634aa699a01a..c0a3833dcd64 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -162,6 +162,9 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call } else if ([@"stopImageStream" isEqualToString:call.method]) { [_camera stopImageStream]; [result sendSuccess]; + } else if ([@"receivedImageStreamData" isEqualToString:call.method]) { + [_camera receivedImageStreamData]; + [result sendSuccess]; } else { NSDictionary *argsMap = call.arguments; NSUInteger cameraId = ((NSNumber *)argsMap[@"cameraId"]).unsignedIntegerValue; diff --git a/packages/camera/camera/ios/Classes/FLTCam.h b/packages/camera/camera/ios/Classes/FLTCam.h index 0cd135e0e41f..8a5dafaf8354 100644 --- a/packages/camera/camera/ios/Classes/FLTCam.h +++ b/packages/camera/camera/ios/Classes/FLTCam.h @@ -61,6 +61,14 @@ NS_ASSUME_NONNULL_BEGIN - (void)setFocusModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr; - (void)applyFocusMode; +/** + * Acknowledges the receipt of one image stream frame. + * + * This should be called each time a frame is received. Failing to call it may + * cause later frames to be dropped instead of streamed. + */ +- (void)receivedImageStreamData; + /** * Applies FocusMode on the AVCaptureDevice. * diff --git a/packages/camera/camera/ios/Classes/FLTCam.m b/packages/camera/camera/ios/Classes/FLTCam.m index 30c177bd4b2a..7af505b249cb 100644 --- a/packages/camera/camera/ios/Classes/FLTCam.m +++ b/packages/camera/camera/ios/Classes/FLTCam.m @@ -10,14 +10,6 @@ @import CoreMotion; #import -@interface FLTImageStreamHandler : NSObject -// The queue on which `eventSink` property should be accessed -@property(nonatomic, strong) dispatch_queue_t captureSessionQueue; -// `eventSink` property should be accessed on `captureSessionQueue`. -// The block itself should be invoked on the main queue. -@property FlutterEventSink eventSink; -@end - @implementation FLTImageStreamHandler - (instancetype)initWithCaptureSessionQueue:(dispatch_queue_t)captureSessionQueue { @@ -68,7 +60,13 @@ @interface FLTCam () *)messenger { + [self startImageStreamWithMessenger:messenger + imageStreamHandler:[[FLTImageStreamHandler alloc] + initWithCaptureSessionQueue:_captureSessionQueue]]; +} + +- (void)startImageStreamWithMessenger:(NSObject *)messenger + imageStreamHandler:(FLTImageStreamHandler *)imageStreamHandler { if (!_isStreamingImages) { FlutterEventChannel *eventChannel = [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/camera/imageStream" @@ -905,12 +916,12 @@ - (void)startImageStreamWithMessenger:(NSObject *)messen FLTThreadSafeEventChannel *threadSafeEventChannel = [[FLTThreadSafeEventChannel alloc] initWithEventChannel:eventChannel]; - _imageStreamHandler = - [[FLTImageStreamHandler alloc] initWithCaptureSessionQueue:_captureSessionQueue]; + _imageStreamHandler = imageStreamHandler; [threadSafeEventChannel setStreamHandler:_imageStreamHandler completion:^{ dispatch_async(self->_captureSessionQueue, ^{ self.isStreamingImages = YES; + self.streamingPendingFramesCount = 0; }); }]; } else { @@ -928,6 +939,10 @@ - (void)stopImageStream { } } +- (void)receivedImageStreamData { + self.streamingPendingFramesCount--; +} + - (void)getMaxZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result { CGFloat maxZoomFactor = [self getMaxAvailableZoomFactor]; diff --git a/packages/camera/camera/ios/Classes/FLTCam_Test.h b/packages/camera/camera/ios/Classes/FLTCam_Test.h index a1f9f2b65981..19e284227f4f 100644 --- a/packages/camera/camera/ios/Classes/FLTCam_Test.h +++ b/packages/camera/camera/ios/Classes/FLTCam_Test.h @@ -5,6 +5,19 @@ #import "FLTCam.h" #import "FLTSavePhotoDelegate.h" +@interface FLTImageStreamHandler : NSObject + +/// The queue on which `eventSink` property should be accessed. +@property(nonatomic, strong) dispatch_queue_t captureSessionQueue; + +/// The event sink to stream camera events to Dart. +/// +/// The property should only be accessed on `captureSessionQueue`. +/// The block itself should be invoked on the main queue. +@property FlutterEventSink eventSink; + +@end + // APIs exposed for unit testing. @interface FLTCam () @@ -14,6 +27,9 @@ /// The output for photo capturing. Exposed setter for unit tests. @property(strong, nonatomic) AVCapturePhotoOutput *capturePhotoOutput API_AVAILABLE(ios(10)); +/// True when images from the camera are being streamed. +@property(assign, nonatomic) BOOL isStreamingImages; + /// A dictionary to retain all in-progress FLTSavePhotoDelegates. The key of the dictionary is the /// AVCapturePhotoSettings's uniqueID for each photo capture operation, and the value is the /// FLTSavePhotoDelegate that handles the result of each photo capture operation. Note that photo @@ -38,4 +54,8 @@ captureSessionQueue:(dispatch_queue_t)captureSessionQueue error:(NSError **)error; +/// Start streaming images. +- (void)startImageStreamWithMessenger:(NSObject *)messenger + imageStreamHandler:(FLTImageStreamHandler *)imageStreamHandler; + @end diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 30e6221697c9..1492ca193761 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -448,6 +448,13 @@ class CameraController extends ValueNotifier { _imageStreamSubscription = cameraEventChannel.receiveBroadcastStream().listen( (dynamic imageData) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + try { + _channel.invokeMethod('receivedImageStreamData'); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } onAvailable( CameraImage.fromPlatformData(imageData as Map)); }, diff --git a/packages/camera/camera/lib/src/camera_image.dart b/packages/camera/camera/lib/src/camera_image.dart index fd3a3d6233bc..0f2377ed170c 100644 --- a/packages/camera/camera/lib/src/camera_image.dart +++ b/packages/camera/camera/lib/src/camera_image.dart @@ -6,7 +6,6 @@ import 'dart:typed_data'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; /// A single color plane of image data. /// diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 2baab09c5dcb..f62777044617 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing Dart. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.4+16 +version: 0.9.4+21 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/camera/camera/test/camera_image_test.dart b/packages/camera/camera/test/camera_image_test.dart index b09a14177121..55bf4a2727e2 100644 --- a/packages/camera/camera/test/camera_image_test.dart +++ b/packages/camera/camera/test/camera_image_test.dart @@ -5,8 +5,6 @@ import 'dart:typed_data'; import 'package:camera/camera.dart'; -import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index 76bfe40605d7..fe2f4f4e35c7 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:camera/camera.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index c4e0c9388231..34a474b2b4f3 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:math'; -import 'dart:ui'; import 'package:camera/camera.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index 62df1fd1a2a1..d718d5e48f02 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -5,7 +5,6 @@ import 'dart:ui'; import 'package:camera/camera.dart'; -import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index 6ed662b2afa0..3cad35d71ae5 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Removes unnecessary imports. + +## 2.1.6 + +* Adopts `Object.hash`. +* Removes obsolete dependency on `pedantic`. + ## 2.1.5 * Fixes asynchronous exceptions handling of the `initializeCamera` method. diff --git a/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart index a91e538e4be5..a6ace8f9ae74 100644 --- a/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart +++ b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:camera_platform_interface/src/types/focus_mode.dart'; import 'package:flutter/foundation.dart' show immutable; import '../../camera_platform_interface.dart'; @@ -117,14 +116,15 @@ class CameraInitializedEvent extends CameraEvent { focusPointSupported == other.focusPointSupported; @override - int get hashCode => - super.hashCode ^ - previewWidth.hashCode ^ - previewHeight.hashCode ^ - exposureMode.hashCode ^ - exposurePointSupported.hashCode ^ - focusMode.hashCode ^ - focusPointSupported.hashCode; + int get hashCode => Object.hash( + super.hashCode, + previewWidth, + previewHeight, + exposureMode, + exposurePointSupported, + focusMode, + focusPointSupported, + ); } /// An event fired when the resolution preset of the camera has changed. @@ -171,8 +171,7 @@ class CameraResolutionChangedEvent extends CameraEvent { captureHeight == other.captureHeight; @override - int get hashCode => - super.hashCode ^ captureWidth.hashCode ^ captureHeight.hashCode; + int get hashCode => Object.hash(super.hashCode, captureWidth, captureHeight); } /// An event fired when the camera is going to close. @@ -239,7 +238,7 @@ class CameraErrorEvent extends CameraEvent { description == other.description; @override - int get hashCode => super.hashCode ^ description.hashCode; + int get hashCode => Object.hash(super.hashCode, description); } /// An event fired when a video has finished recording. @@ -284,6 +283,5 @@ class VideoRecordedEvent extends CameraEvent { maxVideoDuration == other.maxVideoDuration; @override - int get hashCode => - super.hashCode ^ file.hashCode ^ maxVideoDuration.hashCode; + int get hashCode => Object.hash(super.hashCode, file, maxVideoDuration); } diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart index ec84c204b2c6..c856f3467821 100644 --- a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -6,11 +6,7 @@ import 'dart:async'; import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_platform_interface/src/events/device_event.dart'; -import 'package:camera_platform_interface/src/types/focus_mode.dart'; -import 'package:camera_platform_interface/src/types/image_format_group.dart'; import 'package:camera_platform_interface/src/utils/utils.dart'; -import 'package:cross_file/cross_file.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index 0d240496086d..daa19b8b4011 100644 --- a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -6,12 +6,7 @@ import 'dart:async'; import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_platform_interface/src/events/device_event.dart'; import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; -import 'package:camera_platform_interface/src/types/exposure_mode.dart'; -import 'package:camera_platform_interface/src/types/focus_mode.dart'; -import 'package:camera_platform_interface/src/types/image_format_group.dart'; -import 'package:cross_file/cross_file.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; diff --git a/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart b/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart index df7263631252..0167cf9e17a1 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart @@ -50,7 +50,7 @@ class CameraDescription { lensDirection == other.lensDirection; @override - int get hashCode => name.hashCode ^ lensDirection.hashCode; + int get hashCode => Object.hash(name, lensDirection); @override String toString() { diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index b28008d8d58e..ab163b4e9f3f 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.1.5 +version: 2.1.6 environment: sdk: '>=2.12.0 <3.0.0' - flutter: ">=2.0.0" + flutter: ">=2.8.0" dependencies: cross_file: ^0.3.1 @@ -21,4 +21,3 @@ dev_dependencies: async: ^2.5.0 flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/camera/camera_platform_interface/test/events/camera_event_test.dart b/packages/camera/camera_platform_interface/test/events/camera_event_test.dart index a46486ed252c..3914859d44b0 100644 --- a/packages/camera/camera_platform_interface/test/events/camera_event_test.dart +++ b/packages/camera/camera_platform_interface/test/events/camera_event_test.dart @@ -3,8 +3,6 @@ // found in the LICENSE file. import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_platform_interface/src/types/exposure_mode.dart'; -import 'package:camera_platform_interface/src/types/focus_mode.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -137,13 +135,14 @@ void main() { test('hashCode should match hashCode of all properties', () { const CameraInitializedEvent event = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); - final int expectedHashCode = event.cameraId.hashCode ^ - event.previewWidth.hashCode ^ - event.previewHeight.hashCode ^ - event.exposureMode.hashCode ^ - event.exposurePointSupported.hashCode ^ - event.focusMode.hashCode ^ - event.focusPointSupported.hashCode; + final int expectedHashCode = Object.hash( + event.cameraId, + event.previewWidth, + event.previewHeight, + event.exposureMode, + event.exposurePointSupported, + event.focusMode, + event.focusPointSupported); expect(event.hashCode, expectedHashCode); }); @@ -223,9 +222,8 @@ void main() { test('hashCode should match hashCode of all properties', () { const CameraResolutionChangedEvent event = CameraResolutionChangedEvent(1, 1024, 640); - final int expectedHashCode = event.cameraId.hashCode ^ - event.captureWidth.hashCode ^ - event.captureHeight.hashCode; + final int expectedHashCode = + Object.hash(event.cameraId, event.captureWidth, event.captureHeight); expect(event.hashCode, expectedHashCode); }); @@ -328,7 +326,7 @@ void main() { test('hashCode should match hashCode of all properties', () { const CameraErrorEvent event = CameraErrorEvent(1, 'Error'); final int expectedHashCode = - event.cameraId.hashCode ^ event.description.hashCode; + Object.hash(event.cameraId, event.description); expect(event.hashCode, expectedHashCode); }); diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart index 27fe7c6b7166..7da4262cdf79 100644 --- a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -7,11 +7,8 @@ import 'dart:math'; import 'package:async/async.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_platform_interface/src/events/device_event.dart'; import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; -import 'package:camera_platform_interface/src/types/focus_mode.dart'; import 'package:camera_platform_interface/src/utils/utils.dart'; -import 'package:flutter/services.dart' hide DeviceOrientation; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/camera/camera_platform_interface/test/types/camera_description_test.dart b/packages/camera/camera_platform_interface/test/types/camera_description_test.dart index 3d3aaaeb4086..a86df031ac3a 100644 --- a/packages/camera/camera_platform_interface/test/types/camera_description_test.dart +++ b/packages/camera/camera_platform_interface/test/types/camera_description_test.dart @@ -97,15 +97,15 @@ void main() { expect(firstDescription == secondDescription, true); }); - test('hashCode should match hashCode of all properties', () { + test('hashCode should match hashCode of all equality-tested properties', + () { const CameraDescription description = CameraDescription( name: 'Test', lensDirection: CameraLensDirection.front, sensorOrientation: 0, ); - final int expectedHashCode = description.name.hashCode ^ - description.lensDirection.hashCode ^ - description.sensorOrientation.hashCode; + final int expectedHashCode = + Object.hash(description.name, description.lensDirection); expect(description.hashCode, expectedHashCode); }); diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md index 9e486667c103..852e4a03fd43 100644 --- a/packages/camera/camera_web/CHANGELOG.md +++ b/packages/camera/camera_web/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.2.1+4 +* Migrates from `ui.hash*` to `Object.hash*`. * Updates minimum Flutter version for changes in 0.2.1+3. ## 0.2.1+3 diff --git a/packages/camera/camera_web/lib/src/types/camera_metadata.dart b/packages/camera/camera_web/lib/src/types/camera_metadata.dart index c42dd3ad8b75..e5c6b3875b6a 100644 --- a/packages/camera/camera_web/lib/src/types/camera_metadata.dart +++ b/packages/camera/camera_web/lib/src/types/camera_metadata.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter/foundation.dart'; /// Metadata used along the camera description @@ -38,5 +36,5 @@ class CameraMetadata { } @override - int get hashCode => hashValues(deviceId.hashCode, facingMode.hashCode); + int get hashCode => Object.hash(deviceId.hashCode, facingMode.hashCode); } diff --git a/packages/camera/camera_web/lib/src/types/camera_options.dart b/packages/camera/camera_web/lib/src/types/camera_options.dart index 8fa40bdc1bb8..08491b56081b 100644 --- a/packages/camera/camera_web/lib/src/types/camera_options.dart +++ b/packages/camera/camera_web/lib/src/types/camera_options.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter/foundation.dart'; /// Options used to create a camera with the given @@ -50,7 +48,7 @@ class CameraOptions { } @override - int get hashCode => hashValues(audio, video); + int get hashCode => Object.hash(audio, video); } /// Indicates whether the audio track is requested. @@ -140,7 +138,7 @@ class VideoConstraints { } @override - int get hashCode => hashValues(facingMode, width, height, deviceId); + int get hashCode => Object.hash(facingMode, width, height, deviceId); } /// The camera type used in [FacingModeConstraint]. @@ -213,7 +211,7 @@ class FacingModeConstraint { } @override - int get hashCode => hashValues(ideal, exact); + int get hashCode => Object.hash(ideal, exact); } /// The size of the requested video track used in @@ -272,5 +270,5 @@ class VideoSizeConstraint { } @override - int get hashCode => hashValues(minimum, ideal, maximum); + int get hashCode => Object.hash(minimum, ideal, maximum); } diff --git a/packages/camera/camera_web/lib/src/types/zoom_level_capability.dart b/packages/camera/camera_web/lib/src/types/zoom_level_capability.dart index 9a868d2bc0dc..d20bd25108bb 100644 --- a/packages/camera/camera_web/lib/src/types/zoom_level_capability.dart +++ b/packages/camera/camera_web/lib/src/types/zoom_level_capability.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:html' as html; -import 'dart:ui' show hashValues; import 'package:flutter/foundation.dart'; @@ -46,5 +45,5 @@ class ZoomLevelCapability { } @override - int get hashCode => hashValues(minimum, maximum, videoTrack); + int get hashCode => Object.hash(minimum, maximum, videoTrack); } diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index cb6aa19c49f1..2d1a4508eb73 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_web description: A Flutter plugin for getting information about and controlling the camera on Web. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.2.1+3 +version: 0.2.1+4 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/camera/camera_windows/CHANGELOG.md b/packages/camera/camera_windows/CHANGELOG.md index 1318780830f8..b1383dc54993 100644 --- a/packages/camera/camera_windows/CHANGELOG.md +++ b/packages/camera/camera_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Removes unnecessary imports. + ## 0.1.0 * Initial release diff --git a/packages/camera/camera_windows/lib/camera_windows.dart b/packages/camera/camera_windows/lib/camera_windows.dart index 33f8bfb68fac..d998863d43a7 100644 --- a/packages/camera/camera_windows/lib/camera_windows.dart +++ b/packages/camera/camera_windows/lib/camera_windows.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:cross_file/cross_file.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_transform/stream_transform.dart'; diff --git a/packages/espresso/CHANGELOG.md b/packages/espresso/CHANGELOG.md index 3f19984b9437..eb1f267ca1d3 100644 --- a/packages/espresso/CHANGELOG.md +++ b/packages/espresso/CHANGELOG.md @@ -1,6 +1,13 @@ -## NEXT +## 0.2.0+1 + +* Adds OS version support information to README. +* Updates `androidx.test.ext:junit` and `androidx.test.ext:truth` for + compatibilty with updated Flutter template. + +## 0.2.0 * Updates compileSdkVersion to 31. +* **Breaking Change** Update guava version to latest stable: `com.google.guava:guava:31.1-android`. ## 0.1.0+4 diff --git a/packages/espresso/README.md b/packages/espresso/README.md index 7747560682e9..6d66bfbe85b5 100644 --- a/packages/espresso/README.md +++ b/packages/espresso/README.md @@ -2,6 +2,10 @@ Provides bindings for Espresso tests of Flutter Android apps. +| | Android | +|-------------|---------| +| **Support** | SDK 16+ | + ## Installation Add the `espresso` package as a `dev_dependency` in your app's pubspec.yaml. If you're testing the example app of a package, add it as a dev_dependency of the main package as well. @@ -81,13 +85,13 @@ void main() { The following command line command runs the test locally: -``` +```sh ./gradlew app:connectedAndroidTest -Ptarget=`pwd`/../test_driver/example.dart ``` Espresso tests can also be run on [Firebase Test Lab](https://firebase.google.com/docs/test-lab): -``` +```sh ./gradlew app:assembleAndroidTest ./gradlew app:assembleDebug -Ptarget=.dart gcloud auth activate-service-account --key-file= @@ -99,4 +103,3 @@ gcloud firebase test android run --type instrumentation \ --results-bucket= \ --results-dir= ``` - diff --git a/packages/espresso/android/build.gradle b/packages/espresso/android/build.gradle index e5a042095c2f..62baba0ac31b 100644 --- a/packages/espresso/android/build.gradle +++ b/packages/espresso/android/build.gradle @@ -49,7 +49,7 @@ android { } dependencies { - implementation 'com.google.guava:guava:28.1-android' + implementation 'com.google.guava:guava:31.1-android' implementation 'com.squareup.okhttp3:okhttp:3.12.1' implementation 'com.google.code.gson:gson:2.8.6' androidTestImplementation 'org.hamcrest:hamcrest:2.2' @@ -67,8 +67,8 @@ dependencies { api 'androidx.test:rules:1.1.0' // Assertions - api 'androidx.test.ext:junit:1.0.0' - api 'androidx.test.ext:truth:1.0.0' + api 'androidx.test.ext:junit:1.1.3' + api 'androidx.test.ext:truth:1.4.0' api 'com.google.truth:truth:0.42' // Espresso dependencies diff --git a/packages/espresso/pubspec.yaml b/packages/espresso/pubspec.yaml index 1836e7afb575..7737fc46d4b6 100644 --- a/packages/espresso/pubspec.yaml +++ b/packages/espresso/pubspec.yaml @@ -3,7 +3,7 @@ description: Java classes for testing Flutter apps using Espresso. Allows driving Flutter widgets from a native Espresso test. repository: https://github.com/flutter/plugins/tree/main/packages/espresso issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+espresso%22 -version: 0.1.0+4 +version: 0.2.0+1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/file_selector/file_selector/CHANGELOG.md b/packages/file_selector/file_selector/CHANGELOG.md index 65b3a64475e9..c0821fed7446 100644 --- a/packages/file_selector/file_selector/CHANGELOG.md +++ b/packages/file_selector/file_selector/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Removes unnecessary imports. +* Adds OS version support information to README. + ## 0.8.4+1 * Adds README information about macOS entitlements. diff --git a/packages/file_selector/file_selector/README.md b/packages/file_selector/file_selector/README.md index 863164ebf286..89cac1e6fd5f 100644 --- a/packages/file_selector/file_selector/README.md +++ b/packages/file_selector/file_selector/README.md @@ -4,18 +4,22 @@ A Flutter plugin that manages files and interactions with file dialogs. +| | macOS | Web | Windows | +|-------------|--------|-----|-------------| +| **Support** | 10.11+ | Any | Windows 10+ | + ## Usage To use this plugin, add `file_selector` as a [dependency in your pubspec.yaml file](https://flutter.dev/platform-plugins/). ### macOS You will need to [add an entitlement][entitlement] for either read-only access: -``` +```xml com.apple.security.files.user-selected.read-only ``` or read/write access: -``` +```xml com.apple.security.files.user-selected.read-write ``` diff --git a/packages/file_selector/file_selector/test/file_selector_test.dart b/packages/file_selector/file_selector/test/file_selector_test.dart index 6ab0bd975036..fc3e668f9d9e 100644 --- a/packages/file_selector/file_selector/test/file_selector_test.dart +++ b/packages/file_selector/file_selector/test/file_selector_test.dart @@ -6,7 +6,6 @@ import 'package:file_selector/file_selector.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:test/fake.dart'; void main() { late FakeFileSelector fakePlatformImplementation; diff --git a/packages/file_selector/file_selector_macos/CHANGELOG.md b/packages/file_selector/file_selector_macos/CHANGELOG.md index 794d056811f4..b46a174bd323 100644 --- a/packages/file_selector/file_selector_macos/CHANGELOG.md +++ b/packages/file_selector/file_selector_macos/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Removes unnecessary imports. + ## 0.8.2 * Moves source to flutter/plugins. diff --git a/packages/file_selector/file_selector_macos/README.md b/packages/file_selector/file_selector_macos/README.md index efa5272149be..3241b21d1e18 100644 --- a/packages/file_selector/file_selector_macos/README.md +++ b/packages/file_selector/file_selector_macos/README.md @@ -17,12 +17,12 @@ APIs directly. ### Entitlements You will need to [add an entitlement][4] for either read-only access: -``` +```xml com.apple.security.files.user-selected.read-only ``` or read/write access: -``` +```xml com.apple.security.files.user-selected.read-write ``` diff --git a/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart index e321d331961b..e50c296b005f 100644 --- a/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart +++ b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:cross_file/cross_file.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/services.dart'; diff --git a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md index b633bd35a59e..100b6ad136a7 100644 --- a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md +++ b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Removes unnecessary imports. + ## 2.0.4 * Removes dependency on `meta`. diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart b/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart index f1fa82b6f3c6..c6d0f4a56155 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:cross_file/cross_file.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/services.dart'; diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart b/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart index f8fa83bd18d2..a23957af9110 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart @@ -4,7 +4,6 @@ import 'dart:async'; -import 'package:cross_file/cross_file.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; diff --git a/packages/file_selector/file_selector_windows/CHANGELOG.md b/packages/file_selector/file_selector_windows/CHANGELOG.md index 63999f245d82..ae3cd13342b1 100644 --- a/packages/file_selector/file_selector_windows/CHANGELOG.md +++ b/packages/file_selector/file_selector_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Removes unnecessary imports. + ## 0.8.2 * Moves source to flutter/plugins, and restructures to allow for unit testing. diff --git a/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart b/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart index a8b159711e2a..b91a22355572 100644 --- a/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart +++ b/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:cross_file/cross_file.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/services.dart'; diff --git a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md index 8b110b74acfc..8fdfc39f3bf9 100644 --- a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md +++ b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Adds OS version support information to README. + ## 2.0.5 * Updates compileSdkVersion to 31. diff --git a/packages/flutter_plugin_android_lifecycle/README.md b/packages/flutter_plugin_android_lifecycle/README.md index 3290140f4e5e..2475230d413b 100644 --- a/packages/flutter_plugin_android_lifecycle/README.md +++ b/packages/flutter_plugin_android_lifecycle/README.md @@ -9,6 +9,10 @@ The purpose of having this plugin instead of exposing an Android `Lifecycle` obj Android embedding plugins API is to force plugins to have a pub constraint that signifies the major version of the Android `Lifecycle` API they expect. +| | Android | +|-------------|---------| +| **Support** | SDK 16+ | + ## Installation Add `flutter_plugin_android_lifecycle` as a [dependency in your pubspec.yaml file](https://flutter.dev/using-packages/). @@ -32,7 +36,7 @@ public class MyPlugin implements FlutterPlugin, ActivityAware { Lifecycle lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); // Use lifecycle as desired. } - + //... } ``` diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 565bf8412c61..b0662b89d9cc 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,16 @@ +## NEXT + +* Removes unnecessary imports. + +## 2.1.4 + +* Updates Android Google maps sdk version to `18.0.2`. +* Adds OS version support information to README. + +## 2.1.3 + +* Fixes iOS crash on `EXC_BAD_ACCESS KERN_PROTECTION_FAILURE` if the map frame changes long after creation. + ## 2.1.2 * Removes dependencies from `pubspec.yaml` that are only needed in `example/pubspec.yaml` diff --git a/packages/google_maps_flutter/google_maps_flutter/README.md b/packages/google_maps_flutter/google_maps_flutter/README.md index 038126f4fdd0..ae9a659c715f 100644 --- a/packages/google_maps_flutter/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/google_maps_flutter/README.md @@ -4,6 +4,10 @@ A Flutter plugin that provides a [Google Maps](https://developers.google.com/maps/) widget. +| | Android | iOS | +|-------------|---------|--------| +| **Support** | SDK 20+ | iOS 9+ | + ## Usage To use this plugin, add `google_maps_flutter` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). @@ -60,7 +64,7 @@ if (defaultTargetPlatform == TargetPlatform.android) { ### iOS -This plugin requires iOS 9.0 or higher. To set up, specify your API key in the application delegate `ios/Runner/AppDelegate.m`: +To set up, specify your API key in the application delegate `ios/Runner/AppDelegate.m`: ```objectivec #include "AppDelegate.h" diff --git a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle index bf283bea9ef9..356e3c5e22f7 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:3.5.4' } } @@ -35,7 +35,7 @@ android { dependencies { implementation "androidx.annotation:annotation:1.1.0" - implementation 'com.google.android.gms:play-services-maps:17.0.0' + implementation 'com.google.android.gms:play-services-maps:18.0.2' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/CircleControllerTest.java b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/CircleControllerTest.java index 72a8cab626b5..064c8c3591eb 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/CircleControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/CircleControllerTest.java @@ -7,7 +7,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import com.google.android.gms.internal.maps.zzh; +import com.google.android.gms.internal.maps.zzl; import com.google.android.gms.maps.model.Circle; import org.junit.Test; import org.mockito.Mockito; @@ -16,7 +16,7 @@ public class CircleControllerTest { @Test public void controller_SetsStrokeDensity() { - final zzh z = mock(zzh.class); + final zzl z = mock(zzl.class); final Circle circle = spy(new Circle(z)); final float density = 5; diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolygonControllerTest.java b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolygonControllerTest.java index 29234b6adb42..5c73a3f3e449 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolygonControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolygonControllerTest.java @@ -7,7 +7,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import com.google.android.gms.internal.maps.zzw; +import com.google.android.gms.internal.maps.zzaa; import com.google.android.gms.maps.model.Polygon; import org.junit.Test; import org.mockito.Mockito; @@ -16,7 +16,7 @@ public class PolygonControllerTest { @Test public void controller_SetsStrokeDensity() { - final zzw z = mock(zzw.class); + final zzaa z = mock(zzaa.class); final Polygon polygon = spy(new Polygon(z)); final float density = 5; diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolylineControllerTest.java b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolylineControllerTest.java index bb7653aa2293..db570174e215 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolylineControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolylineControllerTest.java @@ -7,7 +7,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import com.google.android.gms.internal.maps.zzz; +import com.google.android.gms.internal.maps.zzad; import com.google.android.gms.maps.model.Polyline; import org.junit.Test; import org.mockito.Mockito; @@ -16,7 +16,7 @@ public class PolylineControllerTest { @Test public void controller_SetsStrokeDensity() { - final zzz z = mock(zzz.class); + final zzad z = mock(zzad.class); final Polyline polyline = spy(new Polyline(z)); final float density = 5; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter/example/android/build.gradle index 456d020f6e2c..4d8d45d13a0b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:3.5.4' } } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart index a007dddd9188..35351505e495 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart @@ -8,7 +8,6 @@ import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:integration_test/integration_test.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile b/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile index 9686afaf3c99..29bfe631a3e7 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile @@ -31,6 +31,8 @@ target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths + + pod 'OCMock', '~> 3.9.1' end end diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj index fbb006aeded0..6a0466c3c6d9 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 982F2A6C27BADE17003C81F4 /* PartiallyMockedMapView.m in Sources */ = {isa = PBXBuildFile; fileRef = 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */; }; F7151F13265D7ED70028CB91 /* GoogleMapsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */; }; F7151F21265D7EE50028CB91 /* GoogleMapsUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F20265D7EE50028CB91 /* GoogleMapsUITests.m */; }; FC8F35FC8CD533B128950487 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */; }; @@ -67,6 +68,8 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 982F2A6A27BADE17003C81F4 /* PartiallyMockedMapView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PartiallyMockedMapView.h; sourceTree = ""; }; + 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PartiallyMockedMapView.m; sourceTree = ""; }; B7AFC65E3DD5AC60D834D83D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; EA0E91726245EDC22B97E8B9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; @@ -188,6 +191,8 @@ isa = PBXGroup; children = ( F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */, + 982F2A6A27BADE17003C81F4 /* PartiallyMockedMapView.h */, + 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */, F7151F14265D7ED70028CB91 /* Info.plist */, ); path = RunnerTests; @@ -270,7 +275,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1320; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -441,6 +446,7 @@ buildActionMask = 2147483647; files = ( F7151F13265D7ED70028CB91 /* GoogleMapsTests.m in Sources */, + 982F2A6C27BADE17003C81F4 /* PartiallyMockedMapView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -511,6 +517,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -567,6 +574,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index afdb55fdfbdd..c983bfc640ff 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ +#import "PartiallyMockedMapView.h" + @interface GoogleMapsTests : XCTestCase @end @@ -15,4 +19,24 @@ - (void)testPlugin { XCTAssertNotNil(plugin); } +- (void)testFrameObserver { + id registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); + CGRect frame = CGRectMake(0, 0, 100, 100); + PartiallyMockedMapView *mapView = [[PartiallyMockedMapView alloc] + initWithFrame:frame + camera:[[GMSCameraPosition alloc] initWithLatitude:0 longitude:0 zoom:0]]; + FLTGoogleMapController *controller = [[FLTGoogleMapController alloc] initWithMapView:mapView + viewIdentifier:0 + arguments:nil + registrar:registrar]; + + for (NSInteger i = 0; i < 10; ++i) { + [controller view]; + } + XCTAssertEqual(mapView.frameObserverCount, 1); + + mapView.frame = frame; + XCTAssertEqual(mapView.frameObserverCount, 0); +} + @end diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/PartiallyMockedMapView.h b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/PartiallyMockedMapView.h new file mode 100644 index 000000000000..4288401cf90d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/PartiallyMockedMapView.h @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import GoogleMaps; + +/** + * Defines a map view used for testing key-value observing. + */ +@interface PartiallyMockedMapView : GMSMapView + +/** + * The number of times that the `frame` KVO has been added. + */ +@property(nonatomic, assign, readonly) NSInteger frameObserverCount; + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/PartiallyMockedMapView.m b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/PartiallyMockedMapView.m new file mode 100644 index 000000000000..202a18d128c0 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/PartiallyMockedMapView.m @@ -0,0 +1,34 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "PartiallyMockedMapView.h" + +@interface PartiallyMockedMapView () + +@property(nonatomic, assign) NSInteger frameObserverCount; + +@end + +@implementation PartiallyMockedMapView + +- (void)addObserver:(NSObject *)observer + forKeyPath:(NSString *)keyPath + options:(NSKeyValueObservingOptions)options + context:(void *)context { + [super addObserver:observer forKeyPath:keyPath options:options context:context]; + + if ([keyPath isEqualToString:@"frame"]) { + ++self.frameObserverCount; + } +} + +- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath { + [super removeObserver:observer forKeyPath:keyPath]; + + if ([keyPath isEqualToString:@"frame"]) { + --self.frameObserverCount; + } +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/lite_mode.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/lite_mode.dart index 0ecc5ed38e87..b1b58fdc91bf 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/lite_mode.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/lite_mode.dart @@ -5,7 +5,6 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_click.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_click.dart index ef1bfe2b11dc..bbe2372dce9a 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_click.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_click.dart @@ -5,7 +5,6 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart index dc4376a19547..8e4853c040ed 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart @@ -5,7 +5,6 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/scrolling_map.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/scrolling_map.dart index 8d046fc6b387..04769315e685 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/scrolling_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/scrolling_map.dart @@ -7,7 +7,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m index df4e8761e6b2..ca8068129566 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -51,7 +51,6 @@ @implementation FLTGoogleMapController { FlutterMethodChannel *_channel; BOOL _trackCameraPosition; NSObject *_registrar; - BOOL _cameraDidInitialSetup; FLTMarkersController *_markersController; FLTPolygonsController *_polygonsController; FLTPolylinesController *_polylinesController; @@ -63,11 +62,19 @@ - (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args registrar:(NSObject *)registrar { + GMSCameraPosition *camera = ToOptionalCameraPosition(args[@"initialCameraPosition"]); + GMSMapView *mapView = [GMSMapView mapWithFrame:frame camera:camera]; + return [self initWithMapView:mapView viewIdentifier:viewId arguments:args registrar:registrar]; +} + +- (instancetype)initWithMapView:(GMSMapView *_Nonnull)mapView + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + registrar:(NSObject *_Nonnull)registrar { if (self = [super init]) { + _mapView = mapView; _viewId = viewId; - GMSCameraPosition *camera = ToOptionalCameraPosition(args[@"initialCameraPosition"]); - _mapView = [GMSMapView mapWithFrame:frame camera:camera]; _mapView.accessibilityElementsHidden = NO; _trackCameraPosition = NO; InterpretMapOptions(args[@"options"], self); @@ -83,7 +90,6 @@ - (instancetype)initWithFrame:(CGRect)frame }]; _mapView.delegate = weakSelf; _registrar = registrar; - _cameraDidInitialSetup = NO; _markersController = [[FLTMarkersController alloc] init:_channel mapView:_mapView registrar:registrar]; @@ -119,12 +125,13 @@ - (instancetype)initWithFrame:(CGRect)frame if ([tileOverlaysToAdd isKindOfClass:[NSArray class]]) { [_tileOverlaysController addTileOverlays:tileOverlaysToAdd]; } + + [_mapView addObserver:self forKeyPath:@"frame" options:0 context:nil]; } return self; } - (UIView *)view { - [_mapView addObserver:self forKeyPath:@"frame" options:0 context:nil]; return _mapView; } @@ -132,11 +139,6 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if (_cameraDidInitialSetup) { - // We only observe the frame for initial setup. - [_mapView removeObserver:self forKeyPath:@"frame"]; - return; - } if (object == _mapView && [keyPath isEqualToString:@"frame"]) { CGRect bounds = _mapView.bounds; if (CGRectEqualToRect(bounds, CGRectZero)) { @@ -146,7 +148,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath // zero. return; } - _cameraDidInitialSetup = YES; + // We only observe the frame for initial setup. [_mapView removeObserver:self forKeyPath:@"frame"]; [_mapView moveCamera:[GMSCameraUpdate setCamera:_mapView.camera]]; } else { diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController_Test.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController_Test.h new file mode 100644 index 000000000000..84f6f7ca485f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController_Test.h @@ -0,0 +1,27 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTGoogleMapController (Test) + +/** + * Initializes a map controller with a concrete map view. + * + * @param mapView A map view that will be displayed by the controller + * @param viewId A unique identifier for the controller. + * @param args Parameters for initialising the map view. + * @param registrar The plugin registrar passed from Flutter. + */ +- (instancetype)initWithMapView:(GMSMapView *)mapView + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + registrar:(NSObject *)registrar; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter-umbrella.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter-umbrella.h new file mode 100644 index 000000000000..50880a2b9e9d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter-umbrella.h @@ -0,0 +1,11 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import +#import + +FOUNDATION_EXPORT double google_maps_flutterVersionNumber; +FOUNDATION_EXPORT const unsigned char google_maps_flutterVersionString[]; diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter.modulemap b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter.modulemap new file mode 100644 index 000000000000..19513f4a7602 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter.modulemap @@ -0,0 +1,10 @@ +framework module google_maps_flutter { + umbrella header "google_maps_flutter-umbrella.h" + + export * + module * { export * } + + explicit module Test { + header "GoogleMapController_Test.h" + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec b/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec index f2ed5fc56ea0..e34919c30484 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec +++ b/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec @@ -14,8 +14,9 @@ Downloaded by pub (not CocoaPods). s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter' } s.documentation_url = 'https://pub.dev/packages/google_maps_flutter' - s.source_files = 'Classes/**/*' + s.source_files = 'Classes/**/*.{h,m}' s.public_header_files = 'Classes/**/*.h' + s.module_map = 'Classes/google_maps_flutter.modulemap' s.dependency 'Flutter' s.dependency 'GoogleMaps' s.static_framework = true diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart index 5b1e67c943dd..9736b8b5e06f 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart @@ -7,7 +7,6 @@ library google_maps_flutter; import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; -import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 849019cbdb2d..c10f9c679a17 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/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. repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.1.2 +version: 2.1.4 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md index d1b4ef8c18cf..b6e15d2ad14c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Removes unnecessary imports. + +## 2.1.6 + +* Migrates from `ui.hash*` to `Object.hash*`. +* Updates minimum Flutter version to 2.5.0. + ## 2.1.5 Removes dependency on `meta`. diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart index 614cbe8e29fb..bb4124612be4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart'; /// Generic Event coming from the native side of Maps. /// diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart index 99f4fddaccd3..9c5cbf5a54f0 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart @@ -14,7 +14,6 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf import 'package:stream_transform/stream_transform.dart'; import '../types/tile_overlay_updates.dart'; -import '../types/utils/tile_overlay.dart'; /// Error thrown when an unknown map ID is provided to a method channel API. class UnknownMapIDError extends Error { diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart index 6b39973134be..b6c7b8be692d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart @@ -11,7 +11,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart index 7cb6369e7f59..89006eba6214 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues, Offset; +import 'dart:ui' show Offset; import 'types.dart'; @@ -99,7 +99,7 @@ class CameraPosition { } @override - int get hashCode => hashValues(bearing, target, tilt, zoom); + int get hashCode => Object.hash(bearing, target, tilt, zoom); @override String toString() => diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart index e2ca635be75e..5bc4ca608f1a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter/foundation.dart' show visibleForTesting; /// A pair of latitude and longitude coordinates, stored as degrees. @@ -55,7 +53,7 @@ class LatLng { } @override - int get hashCode => hashValues(latitude, longitude); + int get hashCode => Object.hash(latitude, longitude); } /// A latitude/longitude aligned rectangle. @@ -132,5 +130,5 @@ class LatLngBounds { } @override - int get hashCode => hashValues(southwest, northeast); + int get hashCode => Object.hash(southwest, northeast); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart index 2e2eefa3d32e..3267cad8d8a2 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues, hashList; - import 'package:flutter/foundation.dart' show objectRuntimeType, setEquals; import 'maps_object.dart'; @@ -114,8 +112,8 @@ class MapsObjectUpdates { } @override - int get hashCode => hashValues(hashList(_objectsToAdd), - hashList(_objectIdsToRemove), hashList(_objectsToChange)); + int get hashCode => Object.hash(Object.hashAll(_objectsToAdd), + Object.hashAll(_objectIdsToRemove), Object.hashAll(_objectsToChange)); @override String toString() { diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart index 5e62f7d9ffba..8057d2962e9e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues, Offset; +import 'dart:ui' show Offset; import 'package:flutter/foundation.dart' show immutable, ValueChanged, VoidCallback; @@ -90,7 +90,7 @@ class InfoWindow { } @override - int get hashCode => hashValues(title.hashCode, snippet, anchor); + int get hashCode => Object.hash(title.hashCode, snippet, anchor); @override String toString() { diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart index 46bc6c12e509..b424aa5c00e4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter/foundation.dart' show immutable; /// Represents a point coordinate in the [GoogleMap]'s view. @@ -42,5 +40,5 @@ class ScreenCoordinate { } @override - int get hashCode => hashValues(x, y); + int get hashCode => Object.hash(x, y); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart index db2b53d63512..80b05272e21d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter/foundation.dart' show immutable; import 'types.dart'; @@ -146,6 +144,6 @@ class TileOverlay implements MapsObject { } @override - int get hashCode => hashValues(tileOverlayId, fadeIn, tileProvider, + int get hashCode => Object.hash(tileOverlayId, fadeIn, tileProvider, transparency, zIndex, visible, tileSize); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart index 38c34fcfd27f..18f88b910b48 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'types.dart'; /// Type of map tiles to display. @@ -97,7 +95,7 @@ class MinMaxZoomPreference { } @override - int get hashCode => hashValues(minZoom, maxZoom); + int get hashCode => Object.hash(minZoom, maxZoom); @override String toString() { diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml index 6125dd43d9f6..8df31fcf626b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_fl issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.1.5 +version: 2.1.6 environment: sdk: '>=2.12.0 <3.0.0' - flutter: ">=2.0.0" + flutter: ">=2.5.0" dependencies: collection: ^1.15.0 diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart index 176f702ff0ff..9ae42ced6c42 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart @@ -5,10 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:google_maps_flutter_platform_interface/src/events/map_event.dart'; -import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'dart:async'; import 'package:async/async.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart index c381f9e30750..bdbaff7e2599 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart @@ -10,7 +10,6 @@ import 'package:mockito/mockito.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; void main() { diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart index f09f70fd769e..73ed1d9d1a90 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues, hashList; - -import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter_platform_interface/src/types/maps_object.dart'; import 'package:google_maps_flutter_platform_interface/src/types/maps_object_updates.dart'; @@ -129,10 +126,10 @@ void main() { TestMapsObjectUpdate.from(previous, current); expect( updates.hashCode, - hashValues( - hashList(updates.objectsToAdd), - hashList(updates.objectIdsToRemove), - hashList(updates.objectsToChange))); + Object.hash( + Object.hashAll(updates.objectsToAdd), + Object.hashAll(updates.objectIdsToRemove), + Object.hashAll(updates.objectsToChange))); }); test('toString', () async { diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart index b95ae50a8f08..e28da7ab79ad 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - -import 'package:flutter/rendering.dart'; import 'package:google_maps_flutter_platform_interface/src/types/maps_object.dart'; import 'package:google_maps_flutter_platform_interface/src/types/maps_object_updates.dart'; @@ -37,7 +34,7 @@ class TestMapsObject implements MapsObject { } @override - int get hashCode => hashValues(mapsId, data); + int get hashCode => Object.hash(mapsId, data); } class TestMapsObjectUpdate extends MapsObjectUpdates { diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart index 3a4c34764ef7..c3ccf695032b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; @@ -130,7 +128,7 @@ void main() { tileSize: 128); expect( tileOverlay.hashCode, - hashValues( + Object.hash( tileOverlay.tileOverlayId, tileOverlay.fadeIn, tileOverlay.tileProvider, diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart index 05be14e1ba0b..fbb345c50563 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues, hashList; - import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter_platform_interface/src/types/tile_overlay.dart'; import 'package:google_maps_flutter_platform_interface/src/types/tile_overlay_updates.dart'; @@ -98,10 +96,10 @@ void main() { TileOverlayUpdates.from(previous, current); expect( updates.hashCode, - hashValues( - hashList(updates.tileOverlaysToAdd), - hashList(updates.tileOverlayIdsToRemove), - hashList(updates.tileOverlaysToChange))); + Object.hash( + Object.hashAll(updates.tileOverlaysToAdd), + Object.hashAll(updates.tileOverlayIdsToRemove), + Object.hashAll(updates.tileOverlaysToChange))); }); test('toString', () async { diff --git a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md index 8a3b94151de2..48908b984b0f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Removes unnecessary imports. + ## 0.3.2+1 * Removes dependency on `meta`. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart index 8a5a62013538..1bf0f10f50c8 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart @@ -10,7 +10,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart' show GoogleMap, GoogleMapController; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart index 0355f2923528..c3079dc2492d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart @@ -10,7 +10,6 @@ import 'dart:js_util'; import 'src/shims/dart_ui.dart' as ui; // Conditionally imports dart:ui in web import 'dart:convert'; -import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index b965434f71d9..3011e9405a1e 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,12 @@ +## 5.3.0 + +* Moves Android and iOS implementations to federated packages. + +## 5.2.5 + +* Migrates from `ui.hash*` to `Object.hash*`. +* Adds OS version support information to README. + ## 5.2.4 * Add doc for iOS auth with SERVER_CLIENT_ID diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index bf3260fd0eb4..ebc4b3f7c977 100644 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -6,6 +6,10 @@ _Note_: This plugin is still under development, and some APIs might not be available yet. [Feedback](https://github.com/flutter/flutter/issues) and [Pull Requests](https://github.com/flutter/plugins/pulls) are most welcome! +| | Android | iOS | Web | +|-------------|---------|--------|-----| +| **Support** | SDK 16+ | iOS 9+ | Any | + ## Platform integration ### Android integration diff --git a/packages/google_sign_in/google_sign_in/android/settings.gradle b/packages/google_sign_in/google_sign_in/android/settings.gradle deleted file mode 100644 index d943fae5ece0..000000000000 --- a/packages/google_sign_in/google_sign_in/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'googlesignin' diff --git a/packages/google_sign_in/google_sign_in/example/ios/Podfile b/packages/google_sign_in/google_sign_in/example/ios/Podfile index e577a3081fe8..56085c312df7 100644 --- a/packages/google_sign_in/google_sign_in/example/ios/Podfile +++ b/packages/google_sign_in/google_sign_in/example/ios/Podfile @@ -29,11 +29,6 @@ flutter_ios_podfile_setup target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - - pod 'OCMock','3.5' - end end post_install do |installer| diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj index 06857ed2bd59..8909bb9b31c6 100644 --- a/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj @@ -16,28 +16,8 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; C2FB9CBA01DB0A2DE5F31E12 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0263E28FA425D1CE928BDE15 /* libPods-Runner.a */; }; - C56D3B06A42F3B35C1F47A43 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 18AD6475292B9C45B529DDC9 /* libPods-RunnerTests.a */; }; - F76AC1A52666D0540040C8BC /* GoogleSignInTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F76AC1A42666D0540040C8BC /* GoogleSignInTests.m */; }; - F76AC1B32666D0610040C8BC /* GoogleSignInUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F76AC1B22666D0610040C8BC /* GoogleSignInUITests.m */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - F76AC1A72666D0540040C8BC /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; - F76AC1B52666D0610040C8BC /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -73,12 +53,6 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F582639B44581540871D9BB0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - F76AC1A22666D0540040C8BC /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F76AC1A42666D0540040C8BC /* GoogleSignInTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleSignInTests.m; sourceTree = ""; }; - F76AC1A62666D0540040C8BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F76AC1B02666D0610040C8BC /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F76AC1B22666D0610040C8BC /* GoogleSignInUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleSignInUITests.m; sourceTree = ""; }; - F76AC1B42666D0610040C8BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -90,21 +64,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F76AC19F2666D0540040C8BC /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - C56D3B06A42F3B35C1F47A43 /* libPods-RunnerTests.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F76AC1AD2666D0610040C8BC /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -135,8 +94,6 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, - F76AC1A32666D0540040C8BC /* RunnerTests */, - F76AC1B12666D0610040C8BC /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, 840012C8B5EDBCF56B0E4AC1 /* Pods */, CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, @@ -147,8 +104,6 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - F76AC1A22666D0540040C8BC /* RunnerTests.xctest */, - F76AC1B02666D0610040C8BC /* RunnerUITests.xctest */, ); name = Products; sourceTree = ""; @@ -187,24 +142,6 @@ name = Frameworks; sourceTree = ""; }; - F76AC1A32666D0540040C8BC /* RunnerTests */ = { - isa = PBXGroup; - children = ( - F76AC1A42666D0540040C8BC /* GoogleSignInTests.m */, - F76AC1A62666D0540040C8BC /* Info.plist */, - ); - path = RunnerTests; - sourceTree = ""; - }; - F76AC1B12666D0610040C8BC /* RunnerUITests */ = { - isa = PBXGroup; - children = ( - F76AC1B22666D0610040C8BC /* GoogleSignInUITests.m */, - F76AC1B42666D0610040C8BC /* Info.plist */, - ); - path = RunnerUITests; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -230,43 +167,6 @@ productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; - F76AC1A12666D0540040C8BC /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = F76AC1AB2666D0540040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 27975964E48117AA65B1D6C7 /* [CP] Check Pods Manifest.lock */, - F76AC19E2666D0540040C8BC /* Sources */, - F76AC19F2666D0540040C8BC /* Frameworks */, - F76AC1A02666D0540040C8BC /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - F76AC1A82666D0540040C8BC /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = F76AC1A22666D0540040C8BC /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - F76AC1AF2666D0610040C8BC /* RunnerUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = F76AC1B72666D0610040C8BC /* Build configuration list for PBXNativeTarget "RunnerUITests" */; - buildPhases = ( - F76AC1AC2666D0610040C8BC /* Sources */, - F76AC1AD2666D0610040C8BC /* Frameworks */, - F76AC1AE2666D0610040C8BC /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - F76AC1B62666D0610040C8BC /* PBXTargetDependency */, - ); - name = RunnerUITests; - productName = RunnerUITests; - productReference = F76AC1B02666D0610040C8BC /* RunnerUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -279,16 +179,6 @@ 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; }; - F76AC1A12666D0540040C8BC = { - CreatedOnToolsVersion = 12.5; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - F76AC1AF2666D0610040C8BC = { - CreatedOnToolsVersion = 12.5; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; - }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -305,8 +195,6 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - F76AC1A12666D0540040C8BC /* RunnerTests */, - F76AC1AF2666D0610040C8BC /* RunnerUITests */, ); }; /* End PBXProject section */ @@ -324,45 +212,9 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F76AC1A02666D0540040C8BC /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F76AC1AE2666D0610040C8BC /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 27975964E48117AA65B1D6C7 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -440,37 +292,8 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F76AC19E2666D0540040C8BC /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F76AC1A52666D0540040C8BC /* GoogleSignInTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F76AC1AC2666D0610040C8BC /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F76AC1B32666D0610040C8BC /* GoogleSignInUITests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - F76AC1A82666D0540040C8BC /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = F76AC1A72666D0540040C8BC /* PBXContainerItemProxy */; - }; - F76AC1B62666D0610040C8BC /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = F76AC1B52666D0610040C8BC /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -641,60 +464,6 @@ }; name = Release; }; - F76AC1A92666D0540040C8BC /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 37E582FF620A90D0EB2C0851 /* Pods-RunnerTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; - INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Debug; - }; - F76AC1AA2666D0540040C8BC /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 45D93D4513839BFEA2AA74FE /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; - INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Release; - }; - F76AC1B82666D0610040C8BC /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = RunnerUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_TARGET_NAME = Runner; - }; - name = Debug; - }; - F76AC1B92666D0610040C8BC /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = RunnerUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_TARGET_NAME = Runner; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -716,24 +485,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - F76AC1AB2666D0540040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F76AC1A92666D0540040C8BC /* Debug */, - F76AC1AA2666D0540040C8BC /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - F76AC1B72666D0610040C8BC /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F76AC1B82666D0610040C8BC /* Debug */, - F76AC1B92666D0610040C8BC /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index 6afd409807fa..228f34b651c5 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:ui' show hashValues; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show PlatformException; @@ -148,7 +147,7 @@ class GoogleSignInAccount implements GoogleIdentity { @override int get hashCode => - hashValues(displayName, email, id, photoUrl, _idToken, serverAuthCode); + Object.hash(displayName, email, id, photoUrl, _idToken, serverAuthCode); @override String toString() { diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index de15d0bb0740..760706f2e7bc 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -3,7 +3,8 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android and iOS. repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 5.2.4 +version: 5.3.0 + environment: sdk: ">=2.14.0 <3.0.0" @@ -13,16 +14,17 @@ flutter: plugin: platforms: android: - package: io.flutter.plugins.googlesignin - pluginClass: GoogleSignInPlugin + default_package: google_sign_in_android ios: - pluginClass: FLTGoogleSignInPlugin + default_package: google_sign_in_ios web: default_package: google_sign_in_web dependencies: flutter: sdk: flutter + google_sign_in_android: ^5.2.5 + google_sign_in_ios: ^5.2.5 google_sign_in_platform_interface: ^2.1.0 google_sign_in_web: ^0.10.0 diff --git a/packages/google_sign_in/google_sign_in_android/AUTHORS b/packages/google_sign_in/google_sign_in_android/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md new file mode 100644 index 000000000000..3ffa6b5b7d6b --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md @@ -0,0 +1,7 @@ +## 5.2.6 + +* Switches to an internal method channel, rather than the default. + +## 5.2.5 + +* Splits from `video_player` as a federated implementation. diff --git a/packages/google_sign_in/google_sign_in_android/LICENSE b/packages/google_sign_in/google_sign_in_android/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/google_sign_in/google_sign_in_android/README.md b/packages/google_sign_in/google_sign_in_android/README.md new file mode 100644 index 000000000000..5c7c70ede917 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/README.md @@ -0,0 +1,11 @@ +# google\_sign\_in\_android + +The Android implementation of [`google_sign_in`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `google_sign_in` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/google_sign_in +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/google_sign_in/google_sign_in/android/build.gradle b/packages/google_sign_in/google_sign_in_android/android/build.gradle similarity index 100% rename from packages/google_sign_in/google_sign_in/android/build.gradle rename to packages/google_sign_in/google_sign_in_android/android/build.gradle diff --git a/packages/google_sign_in/google_sign_in_android/android/settings.gradle b/packages/google_sign_in/google_sign_in_android/android/settings.gradle new file mode 100644 index 000000000000..35ebd0e2428a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'google_sign_in_android' diff --git a/packages/google_sign_in/google_sign_in/android/src/main/AndroidManifest.xml b/packages/google_sign_in/google_sign_in_android/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/google_sign_in/google_sign_in/android/src/main/AndroidManifest.xml rename to packages/google_sign_in/google_sign_in_android/android/src/main/AndroidManifest.xml diff --git a/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java similarity index 100% rename from packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java rename to packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java diff --git a/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/Executors.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Executors.java similarity index 100% rename from packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/Executors.java rename to packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Executors.java diff --git a/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java similarity index 99% rename from packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java rename to packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index 1be023c678bb..a1237f0013a1 100644 --- a/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -44,7 +44,7 @@ /** Google sign-in plugin for Flutter. */ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware { - private static final String CHANNEL_NAME = "plugins.flutter.io/google_sign_in"; + private static final String CHANNEL_NAME = "plugins.flutter.io/google_sign_in_android"; private static final String METHOD_INIT = "init"; private static final String METHOD_SIGN_IN_SILENTLY = "signInSilently"; diff --git a/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java similarity index 100% rename from packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java rename to packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java diff --git a/packages/google_sign_in/google_sign_in/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java similarity index 100% rename from packages/google_sign_in/google_sign_in/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java rename to packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java diff --git a/packages/google_sign_in/google_sign_in_android/example/README.md b/packages/google_sign_in/google_sign_in_android/example/README.md new file mode 100644 index 000000000000..79d99dc72982 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/README.md @@ -0,0 +1,8 @@ +# google_sign_in_android example + +Exercises the Android implementation of `GoogleSignInPlatform`. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/build.gradle b/packages/google_sign_in/google_sign_in_android/example/android/app/build.gradle new file mode 100644 index 000000000000..8ac99fe56f3a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/build.gradle @@ -0,0 +1,67 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 31 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "io.flutter.plugins.googlesigninexample" + minSdkVersion 16 + targetSdkVersion 28 + multiDexEnabled true + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } + + testOptions { + unitTests.returnDefaultValues = true + } +} + +flutter { + source '../..' +} + +dependencies { + implementation 'com.google.android.gms:play-services-auth:16.0.1' + testImplementation'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + api 'androidx.test:core:1.2.0' +} diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/google-services.json b/packages/google_sign_in/google_sign_in_android/example/android/app/google-services.json new file mode 100644 index 000000000000..efa524535553 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/google-services.json @@ -0,0 +1,246 @@ +{ + "project_info": { + "project_number": "479882132969", + "firebase_url": "https://my-flutter-proj.firebaseio.com", + "project_id": "my-flutter-proj", + "storage_bucket": "my-flutter-proj.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:479882132969:android:c73fd19ff7e2c0be", + "android_client_info": { + "package_name": "io.flutter.plugins.cameraexample" + } + }, + "oauth_client": [ + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" + } + ], + "services": { + "analytics_service": { + "status": 1 + }, + "appinvite_service": { + "status": 1, + "other_platform_oauth_client": [] + }, + "ads_service": { + "status": 2 + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:479882132969:android:632cdf3fc0a17139", + "android_client_info": { + "package_name": "io.flutter.plugins.firebasedynamiclinksexample" + } + }, + "oauth_client": [ + { + "client_id": "479882132969-32qusitiag53931ck80h121ajhlc5a7e.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "io.flutter.plugins.firebasedynamiclinksexample", + "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0" + } + }, + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" + } + ], + "services": { + "analytics_service": { + "status": 1 + }, + "appinvite_service": { + "status": 2, + "other_platform_oauth_client": [ + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseMlVisionExample" + } + } + ] + }, + "ads_service": { + "status": 2 + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:479882132969:android:ae50362b4bc06086", + "android_client_info": { + "package_name": "io.flutter.plugins.firebasemlvisionexample" + } + }, + "oauth_client": [ + { + "client_id": "479882132969-9pp74fkgmtvt47t9rikc1p861v7n85tn.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "io.flutter.plugins.firebasemlvisionexample", + "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0" + } + }, + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" + } + ], + "services": { + "analytics_service": { + "status": 1 + }, + "appinvite_service": { + "status": 2, + "other_platform_oauth_client": [ + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseMlVisionExample" + } + } + ] + }, + "ads_service": { + "status": 2 + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:479882132969:android:215a22700e1b466b", + "android_client_info": { + "package_name": "io.flutter.plugins.firebaseperformanceexample" + } + }, + "oauth_client": [ + { + "client_id": "479882132969-8h4kiv8m7ho4tvn6uuujsfcrf69unuf7.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "io.flutter.plugins.firebaseperformanceexample", + "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0" + } + }, + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" + } + ], + "services": { + "analytics_service": { + "status": 1 + }, + "appinvite_service": { + "status": 2, + "other_platform_oauth_client": [ + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseMlVisionExample" + } + } + ] + }, + "ads_service": { + "status": 2 + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:479882132969:android:5e9f1f89e134dc86", + "android_client_info": { + "package_name": "io.flutter.plugins.googlesigninexample" + } + }, + "oauth_client": [ + { + "client_id": "479882132969-90ml692hkonp587sl0v0rurmnvkekgrg.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "io.flutter.plugins.googlesigninexample", + "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0" + } + }, + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" + } + ], + "services": { + "analytics_service": { + "status": 1 + }, + "appinvite_service": { + "status": 2, + "other_platform_oauth_client": [ + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseMlVisionExample" + } + } + ] + }, + "ads_service": { + "status": 2 + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/google_sign_in/google_sign_in_android/example/android/app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..9a4163a4f5ee --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java b/packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java new file mode 100644 index 000000000000..edc01de491af --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlesigninexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/GoogleSignInTest.java similarity index 100% rename from packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/GoogleSignInTest.java rename to packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/GoogleSignInTest.java diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/debug/AndroidManifest.xml b/packages/google_sign_in/google_sign_in_android/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..4d764900a530 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/AndroidManifest.xml b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..22a34d7218f7 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/java/io/flutter/plugins/.gitignore b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/java/io/flutter/plugins/.gitignore new file mode 100644 index 000000000000..9eb4563d2ae1 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/java/io/flutter/plugins/.gitignore @@ -0,0 +1 @@ +GeneratedPluginRegistrant.java diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/GoogleSignInTestActivity.java b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/GoogleSignInTestActivity.java new file mode 100644 index 000000000000..09506a2632df --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/GoogleSignInTestActivity.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlesigninexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Makes the FlutterEngine accessible for testing. +public class GoogleSignInTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..17987b79bb8a Binary files /dev/null and b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..09d4391482be Binary files /dev/null and b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/values/strings.xml b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000000..c7e28ffcedd1 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + YOUR_WEB_CLIENT_ID + diff --git a/packages/google_sign_in/google_sign_in_android/example/android/build.gradle b/packages/google_sign_in/google_sign_in_android/example/android/build.gradle new file mode 100644 index 000000000000..e101ac08df55 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.3.0' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/google_sign_in/google_sign_in_android/example/android/gradle.properties b/packages/google_sign_in/google_sign_in_android/example/android/gradle.properties new file mode 100644 index 000000000000..d12b9a8297e5 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true diff --git a/packages/google_sign_in/google_sign_in_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/google_sign_in/google_sign_in_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..019065d1d650 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/packages/google_sign_in/google_sign_in_android/example/android/settings.gradle b/packages/google_sign_in/google_sign_in_android/example/android/settings.gradle new file mode 100644 index 000000000000..115da6cb4f4d --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withInputStream { stream -> plugins.load(stream) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/packages/google_sign_in/google_sign_in_android/example/integration_test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in_android/example/integration_test/google_sign_in_test.dart new file mode 100644 index 000000000000..f1388ce86d67 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/integration_test/google_sign_in_test.dart @@ -0,0 +1,24 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can initialize the plugin', (WidgetTester tester) async { + final GoogleSignInPlatform signIn = GoogleSignInPlatform.instance; + expect(signIn, isNotNull); + }); + + testWidgets('Method channel handler is present', (WidgetTester tester) async { + // isSignedIn can be called without initialization, so use it to validate + // that the native method handler is present (e.g., that the channel name + // is correct). + final GoogleSignInPlatform signIn = GoogleSignInPlatform.instance; + await expectLater(signIn.isSignedIn(), completes); + }); +} diff --git a/packages/google_sign_in/google_sign_in_android/example/lib/main.dart b/packages/google_sign_in/google_sign_in_android/example/lib/main.dart new file mode 100644 index 000000000000..a750c330001d --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/lib/main.dart @@ -0,0 +1,179 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:convert' show json; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:http/http.dart' as http; + +void main() { + runApp( + MaterialApp( + title: 'Google Sign In', + home: SignInDemo(), + ), + ); +} + +class SignInDemo extends StatefulWidget { + @override + State createState() => SignInDemoState(); +} + +class SignInDemoState extends State { + GoogleSignInUserData? _currentUser; + String _contactText = ''; + // Future that completes when `init` has completed on the sign in instance. + Future? _initialization; + + @override + void initState() { + super.initState(); + _signIn(); + } + + Future _ensureInitialized() { + return _initialization ??= GoogleSignInPlatform.instance.init( + scopes: [ + 'email', + 'https://www.googleapis.com/auth/contacts.readonly', + ], + )..catchError((dynamic _) { + _initialization = null; + }); + } + + void _setUser(GoogleSignInUserData? user) { + setState(() { + _currentUser = user; + if (user != null) { + _handleGetContact(user); + } + }); + } + + Future _signIn() async { + await _ensureInitialized(); + final GoogleSignInUserData? newUser = + await GoogleSignInPlatform.instance.signInSilently(); + _setUser(newUser); + } + + Future> _getAuthHeaders() async { + final GoogleSignInUserData? user = _currentUser; + if (user == null) { + throw StateError('No user signed in'); + } + + final GoogleSignInTokenData response = + await GoogleSignInPlatform.instance.getTokens( + email: user.email, + shouldRecoverAuth: true, + ); + + return { + 'Authorization': 'Bearer ${response.accessToken}', + // TODO(kevmoo): Use the correct value once it's available. + // See https://github.com/flutter/flutter/issues/80905 + 'X-Goog-AuthUser': '0', + }; + } + + Future _handleGetContact(GoogleSignInUserData user) async { + setState(() { + _contactText = 'Loading contact info...'; + }); + final http.Response response = await http.get( + Uri.parse('https://people.googleapis.com/v1/people/me/connections' + '?requestMask.includeField=person.names'), + headers: await _getAuthHeaders(), + ); + if (response.statusCode != 200) { + setState(() { + _contactText = 'People API gave a ${response.statusCode} ' + 'response. Check logs for details.'; + }); + print('People API ${response.statusCode} response: ${response.body}'); + return; + } + final Map data = + json.decode(response.body) as Map; + final int contactCount = + (data['connections'] as List?)?.length ?? 0; + setState(() { + _contactText = '$contactCount contacts found'; + }); + } + + Future _handleSignIn() async { + try { + await _ensureInitialized(); + _setUser(await GoogleSignInPlatform.instance.signIn()); + } catch (error) { + final bool canceled = + error is PlatformException && error.code == 'sign_in_canceled'; + if (!canceled) { + print(error); + } + } + } + + Future _handleSignOut() async { + await _ensureInitialized(); + await GoogleSignInPlatform.instance.disconnect(); + } + + Widget _buildBody() { + final GoogleSignInUserData? user = _currentUser; + if (user != null) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ListTile( + title: Text(user.displayName ?? ''), + subtitle: Text(user.email), + ), + const Text('Signed in successfully.'), + Text(_contactText), + ElevatedButton( + child: const Text('SIGN OUT'), + onPressed: _handleSignOut, + ), + ElevatedButton( + child: const Text('REFRESH'), + onPressed: () => _handleGetContact(user), + ), + ], + ); + } else { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const Text('You are not currently signed in.'), + ElevatedButton( + child: const Text('SIGN IN'), + onPressed: _handleSignIn, + ), + ], + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Google Sign In'), + ), + body: ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: _buildBody(), + )); + } +} diff --git a/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml new file mode 100644 index 000000000000..316cdd893a2c --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml @@ -0,0 +1,30 @@ +name: google_sign_in_example +description: Example of Google Sign-In plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + google_sign_in_android: + # When depending on this package from a real application you should use: + # google_sign_in_android: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + google_sign_in_platform_interface: ^2.1.0 + http: ^0.13.0 + +dev_dependencies: + espresso: ^0.1.0+2 + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/google_sign_in/google_sign_in_android/example/test_driver/integration_test.dart b/packages/google_sign_in/google_sign_in_android/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart new file mode 100644 index 000000000000..d96328de695a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart @@ -0,0 +1,95 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:flutter/services.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; + +import 'src/utils.dart'; + +/// Android implementation of [GoogleSignInPlatform]. +class GoogleSignInAndroid extends GoogleSignInPlatform { + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + @visibleForTesting + MethodChannel channel = + const MethodChannel('plugins.flutter.io/google_sign_in_android'); + + /// Registers this class as the default instance of [GoogleSignInPlatform]. + static void registerWith() { + GoogleSignInPlatform.instance = GoogleSignInAndroid(); + } + + @override + Future init({ + List scopes = const [], + SignInOption signInOption = SignInOption.standard, + String? hostedDomain, + String? clientId, + }) { + return channel.invokeMethod('init', { + 'signInOption': signInOption.toString(), + 'scopes': scopes, + 'hostedDomain': hostedDomain, + 'clientId': clientId, + }); + } + + @override + Future signInSilently() { + return channel + .invokeMapMethod('signInSilently') + .then(getUserDataFromMap); + } + + @override + Future signIn() { + return channel + .invokeMapMethod('signIn') + .then(getUserDataFromMap); + } + + @override + Future getTokens( + {required String email, bool? shouldRecoverAuth = true}) { + return channel + .invokeMapMethod('getTokens', { + 'email': email, + 'shouldRecoverAuth': shouldRecoverAuth, + }).then((Map? result) => getTokenDataFromMap(result!)); + } + + @override + Future signOut() { + return channel.invokeMapMethod('signOut'); + } + + @override + Future disconnect() { + return channel.invokeMapMethod('disconnect'); + } + + @override + Future isSignedIn() async { + return (await channel.invokeMethod('isSignedIn'))!; + } + + @override + Future clearAuthCache({String? token}) { + return channel.invokeMethod( + 'clearAuthCache', + {'token': token}, + ); + } + + @override + Future requestScopes(List scopes) async { + return (await channel.invokeMethod( + 'requestScopes', + >{'scopes': scopes}, + ))!; + } +} diff --git a/packages/google_sign_in/google_sign_in_android/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_android/lib/src/utils.dart new file mode 100644 index 000000000000..5cd7c20b829a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/lib/src/utils.dart @@ -0,0 +1,28 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; + +/// Converts user data coming from native code into the proper platform interface type. +GoogleSignInUserData? getUserDataFromMap(Map? data) { + if (data == null) { + return null; + } + return GoogleSignInUserData( + email: data['email']! as String, + id: data['id']! as String, + displayName: data['displayName'] as String?, + photoUrl: data['photoUrl'] as String?, + idToken: data['idToken'] as String?, + serverAuthCode: data['serverAuthCode'] as String?); +} + +/// Converts token data coming from native code into the proper platform interface type. +GoogleSignInTokenData getTokenDataFromMap(Map data) { + return GoogleSignInTokenData( + idToken: data['idToken'] as String?, + accessToken: data['accessToken'] as String?, + serverAuthCode: data['serverAuthCode'] as String?, + ); +} diff --git a/packages/google_sign_in/google_sign_in_android/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/pubspec.yaml new file mode 100644 index 000000000000..fa3dc1489b26 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/pubspec.yaml @@ -0,0 +1,36 @@ +name: google_sign_in_android +description: Android implementation of the google_sign_in plugin. +repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in_android +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 +version: 5.2.6 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: google_sign_in + platforms: + android: + dartPluginClass: GoogleSignInAndroid + package: io.flutter.plugins.googlesignin + pluginClass: GoogleSignInPlugin + +dependencies: + flutter: + sdk: flutter + google_sign_in_platform_interface: ^2.1.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +# The example deliberately includes limited-use secrets. +false_secrets: + - /example/android/app/google-services.json + - /example/lib/main.dart diff --git a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart new file mode 100644 index 000000000000..7d39ae5f0232 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart @@ -0,0 +1,143 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_android/google_sign_in_android.dart'; +import 'package:google_sign_in_android/src/utils.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; + +const Map kUserData = { + 'email': 'john.doe@gmail.com', + 'id': '8162538176523816253123', + 'photoUrl': 'https://lh5.googleusercontent.com/photo.jpg', + 'displayName': 'John Doe', + 'idToken': '123', + 'serverAuthCode': '789', +}; + +const Map kTokenData = { + 'idToken': '123', + 'accessToken': '456', + 'serverAuthCode': '789', +}; + +const Map kDefaultResponses = { + 'init': null, + 'signInSilently': kUserData, + 'signIn': kUserData, + 'signOut': null, + 'disconnect': null, + 'isSignedIn': true, + 'getTokens': kTokenData, + 'requestScopes': true, +}; + +final GoogleSignInUserData? kUser = getUserDataFromMap(kUserData); +final GoogleSignInTokenData kToken = + getTokenDataFromMap(kTokenData as Map); + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final GoogleSignInAndroid googleSignIn = GoogleSignInAndroid(); + final MethodChannel channel = googleSignIn.channel; + + final List log = []; + late Map + responses; // Some tests mutate some kDefaultResponses + + setUp(() { + responses = Map.from(kDefaultResponses); + channel.setMockMethodCallHandler((MethodCall methodCall) { + log.add(methodCall); + final dynamic response = responses[methodCall.method]; + if (response != null && response is Exception) { + return Future.error('$response'); + } + return Future.value(response); + }); + log.clear(); + }); + + test('registered instance', () { + GoogleSignInAndroid.registerWith(); + expect(GoogleSignInPlatform.instance, isA()); + }); + + test('signInSilently transforms platform data to GoogleSignInUserData', + () async { + final dynamic response = await googleSignIn.signInSilently(); + expect(response, kUser); + }); + test('signInSilently Exceptions -> throws', () async { + responses['signInSilently'] = Exception('Not a user'); + expect(googleSignIn.signInSilently(), + throwsA(isInstanceOf())); + }); + + test('signIn transforms platform data to GoogleSignInUserData', () async { + final dynamic response = await googleSignIn.signIn(); + expect(response, kUser); + }); + test('signIn Exceptions -> throws', () async { + responses['signIn'] = Exception('Not a user'); + expect(googleSignIn.signIn(), throwsA(isInstanceOf())); + }); + + test('getTokens transforms platform data to GoogleSignInTokenData', () async { + final dynamic response = await googleSignIn.getTokens( + email: 'example@example.com', shouldRecoverAuth: false); + expect(response, kToken); + expect( + log[0], + isMethodCall('getTokens', arguments: { + 'email': 'example@example.com', + 'shouldRecoverAuth': false, + })); + }); + + test('Other functions pass through arguments to the channel', () async { + final Map tests = { + () { + googleSignIn.init( + hostedDomain: 'example.com', + scopes: ['two', 'scopes'], + signInOption: SignInOption.games, + clientId: 'fakeClientId'); + }: isMethodCall('init', arguments: { + 'hostedDomain': 'example.com', + 'scopes': ['two', 'scopes'], + 'signInOption': 'SignInOption.games', + 'clientId': 'fakeClientId', + }), + () { + googleSignIn.getTokens( + email: 'example@example.com', shouldRecoverAuth: false); + }: isMethodCall('getTokens', arguments: { + 'email': 'example@example.com', + 'shouldRecoverAuth': false, + }), + () { + googleSignIn.clearAuthCache(token: 'abc'); + }: isMethodCall('clearAuthCache', arguments: { + 'token': 'abc', + }), + () { + googleSignIn.requestScopes(['newScope', 'anotherScope']); + }: isMethodCall('requestScopes', arguments: { + 'scopes': ['newScope', 'anotherScope'], + }), + googleSignIn.signOut: isMethodCall('signOut', arguments: null), + googleSignIn.disconnect: isMethodCall('disconnect', arguments: null), + googleSignIn.isSignedIn: isMethodCall('isSignedIn', arguments: null), + }; + + for (final Function f in tests.keys) { + f(); + } + + expect(log, tests.values); + }); +} diff --git a/packages/google_sign_in/google_sign_in_ios/AUTHORS b/packages/google_sign_in/google_sign_in_ios/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md b/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md new file mode 100644 index 000000000000..3ffa6b5b7d6b --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md @@ -0,0 +1,7 @@ +## 5.2.6 + +* Switches to an internal method channel, rather than the default. + +## 5.2.5 + +* Splits from `video_player` as a federated implementation. diff --git a/packages/google_sign_in/google_sign_in_ios/LICENSE b/packages/google_sign_in/google_sign_in_ios/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/google_sign_in/google_sign_in_ios/README.md b/packages/google_sign_in/google_sign_in_ios/README.md new file mode 100644 index 000000000000..25e08fdb4040 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/README.md @@ -0,0 +1,11 @@ +# google\_sign\_in\_ios + +The iOS implementation of [`google_sign_in`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `google_sign_in` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/google_sign_in +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/google_sign_in/google_sign_in_ios/example/README.md b/packages/google_sign_in/google_sign_in_ios/example/README.md new file mode 100644 index 000000000000..ca3dc7023fc9 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/README.md @@ -0,0 +1,8 @@ +# google_sign_in_ios example + +Exercises the iOS implementation of `GoogleSignInPlatform`. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/google_sign_in/google_sign_in_ios/example/integration_test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in_ios/example/integration_test/google_sign_in_test.dart new file mode 100644 index 000000000000..f1388ce86d67 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/integration_test/google_sign_in_test.dart @@ -0,0 +1,24 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can initialize the plugin', (WidgetTester tester) async { + final GoogleSignInPlatform signIn = GoogleSignInPlatform.instance; + expect(signIn, isNotNull); + }); + + testWidgets('Method channel handler is present', (WidgetTester tester) async { + // isSignedIn can be called without initialization, so use it to validate + // that the native method handler is present (e.g., that the channel name + // is correct). + final GoogleSignInPlatform signIn = GoogleSignInPlatform.instance; + await expectLater(signIn.isSignedIn(), completes); + }); +} diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 9.0 + + diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/Debug.xcconfig b/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..9803018ca79d --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Generated.xcconfig" +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/Release.xcconfig b/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..a4a8c604e13d --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Generated.xcconfig" +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" diff --git a/packages/google_sign_in/google_sign_in/example/ios/RunnerTests/Info.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/GoogleSignInPluginTest/Info.plist similarity index 100% rename from packages/google_sign_in/google_sign_in/example/ios/RunnerTests/Info.plist rename to packages/google_sign_in/google_sign_in_ios/example/ios/GoogleSignInPluginTest/Info.plist diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Podfile b/packages/google_sign_in/google_sign_in_ios/example/ios/Podfile new file mode 100644 index 000000000000..e577a3081fe8 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Podfile @@ -0,0 +1,47 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + + pod 'OCMock','3.5' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |build_configuration| + # GoogleSignIn does not support arm64 simulators. + build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386' + end + end +end diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..f2bf4ebc514e --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,740 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 5C6F5A6E1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6F5A6D1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m */; }; + 7A303C2E1E89D76400B1F19E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7A303C2D1E89D76400B1F19E /* GoogleService-Info.plist */; }; + 7ACDFB0E1E8944C400BE2D00 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7ACDFB0D1E8944C400BE2D00 /* AppFrameworkInfo.plist */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + C2FB9CBA01DB0A2DE5F31E12 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0263E28FA425D1CE928BDE15 /* libPods-Runner.a */; }; + C56D3B06A42F3B35C1F47A43 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 18AD6475292B9C45B529DDC9 /* libPods-RunnerTests.a */; }; + F76AC1A52666D0540040C8BC /* GoogleSignInTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F76AC1A42666D0540040C8BC /* GoogleSignInTests.m */; }; + F76AC1B32666D0610040C8BC /* GoogleSignInUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F76AC1B22666D0610040C8BC /* GoogleSignInUITests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F76AC1A72666D0540040C8BC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; + F76AC1B52666D0610040C8BC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0263E28FA425D1CE928BDE15 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 18AD6475292B9C45B529DDC9 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 37E582FF620A90D0EB2C0851 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 45D93D4513839BFEA2AA74FE /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 5A76713E622F06379AEDEBFA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 5C6F5A6C1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 5C6F5A6D1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 7A303C2D1E89D76400B1F19E /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 7ACDFB0D1E8944C400BE2D00 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F582639B44581540871D9BB0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + F76AC1A22666D0540040C8BC /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F76AC1A42666D0540040C8BC /* GoogleSignInTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleSignInTests.m; sourceTree = ""; }; + F76AC1A62666D0540040C8BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F76AC1B02666D0610040C8BC /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F76AC1B22666D0610040C8BC /* GoogleSignInUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleSignInUITests.m; sourceTree = ""; }; + F76AC1B42666D0610040C8BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C2FB9CBA01DB0A2DE5F31E12 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC19F2666D0540040C8BC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C56D3B06A42F3B35C1F47A43 /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC1AD2666D0610040C8BC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { + isa = PBXGroup; + children = ( + 5A76713E622F06379AEDEBFA /* Pods-Runner.debug.xcconfig */, + F582639B44581540871D9BB0 /* Pods-Runner.release.xcconfig */, + 37E582FF620A90D0EB2C0851 /* Pods-RunnerTests.debug.xcconfig */, + 45D93D4513839BFEA2AA74FE /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 7ACDFB0D1E8944C400BE2D00 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + F76AC1A32666D0540040C8BC /* RunnerTests */, + F76AC1B12666D0610040C8BC /* RunnerUITests */, + 97C146EF1CF9000F007C117D /* Products */, + 840012C8B5EDBCF56B0E4AC1 /* Pods */, + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + F76AC1A22666D0540040C8BC /* RunnerTests.xctest */, + F76AC1B02666D0610040C8BC /* RunnerUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 5C6F5A6C1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.h */, + 5C6F5A6D1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m */, + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 7A303C2D1E89D76400B1F19E /* GoogleService-Info.plist */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0263E28FA425D1CE928BDE15 /* libPods-Runner.a */, + 18AD6475292B9C45B529DDC9 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + F76AC1A32666D0540040C8BC /* RunnerTests */ = { + isa = PBXGroup; + children = ( + F76AC1A42666D0540040C8BC /* GoogleSignInTests.m */, + F76AC1A62666D0540040C8BC /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + F76AC1B12666D0610040C8BC /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + F76AC1B22666D0610040C8BC /* GoogleSignInUITests.m */, + F76AC1B42666D0610040C8BC /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 532EA9D341340B1DCD08293D /* [CP] Copy Pods Resources */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; + F76AC1A12666D0540040C8BC /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F76AC1AB2666D0540040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 27975964E48117AA65B1D6C7 /* [CP] Check Pods Manifest.lock */, + F76AC19E2666D0540040C8BC /* Sources */, + F76AC19F2666D0540040C8BC /* Frameworks */, + F76AC1A02666D0540040C8BC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F76AC1A82666D0540040C8BC /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = F76AC1A22666D0540040C8BC /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + F76AC1AF2666D0610040C8BC /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F76AC1B72666D0610040C8BC /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + F76AC1AC2666D0610040C8BC /* Sources */, + F76AC1AD2666D0610040C8BC /* Frameworks */, + F76AC1AE2666D0610040C8BC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F76AC1B62666D0610040C8BC /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = F76AC1B02666D0610040C8BC /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + F76AC1A12666D0540040C8BC = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + F76AC1AF2666D0610040C8BC = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + F76AC1A12666D0540040C8BC /* RunnerTests */, + F76AC1AF2666D0610040C8BC /* RunnerUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7A303C2E1E89D76400B1F19E /* GoogleService-Info.plist in Resources */, + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 7ACDFB0E1E8944C400BE2D00 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC1A02666D0540040C8BC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC1AE2666D0610040C8BC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 27975964E48117AA65B1D6C7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin\n"; + }; + 532EA9D341340B1DCD08293D /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 5C6F5A6E1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC19E2666D0540040C8BC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F76AC1A52666D0540040C8BC /* GoogleSignInTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC1AC2666D0610040C8BC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F76AC1B32666D0610040C8BC /* GoogleSignInUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F76AC1A82666D0540040C8BC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F76AC1A72666D0540040C8BC /* PBXContainerItemProxy */; + }; + F76AC1B62666D0610040C8BC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F76AC1B52666D0610040C8BC /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.googleSignInExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.googleSignInExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + F76AC1A92666D0540040C8BC /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 37E582FF620A90D0EB2C0851 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + F76AC1AA2666D0540040C8BC /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 45D93D4513839BFEA2AA74FE /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; + F76AC1B82666D0610040C8BC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + F76AC1B92666D0610040C8BC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = Runner; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F76AC1AB2666D0540040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F76AC1A92666D0540040C8BC /* Debug */, + F76AC1AA2666D0540040C8BC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F76AC1B72666D0610040C8BC /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F76AC1B82666D0610040C8BC /* Debug */, + F76AC1B92666D0610040C8BC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..f4569c48ce10 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/AppDelegate.h b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/AppDelegate.h @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/AppDelegate.m b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..30b87969f44a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/AppDelegate.m @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d22f10b2ab63 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000000..28c6bf03016f Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000000..f091b6b0bca8 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000000..4cde12118dda Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000000..d0ef06e7edb8 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000000..dcdc2306c285 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000000..c8f9ed8f5cee Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000000..75b2d164a5a9 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000000..c4df70d39da7 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000000..6a84f41e14e2 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000000..d0e1f5853602 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..ebf48f603974 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Base.lproj/Main.storyboard b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/GoogleService-Info.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/GoogleService-Info.plist new file mode 100644 index 000000000000..6042aab908af --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,44 @@ + + + + + AD_UNIT_ID_FOR_BANNER_TEST + ca-app-pub-3940256099942544/2934735716 + AD_UNIT_ID_FOR_INTERSTITIAL_TEST + ca-app-pub-3940256099942544/4411468910 + CLIENT_ID + 479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u + ANDROID_CLIENT_ID + 479882132969-jie8r1me6dsra60pal6ejaj8dgme3tg0.apps.googleusercontent.com + API_KEY + AIzaSyBECOwLTAN6PU4Aet1b2QLGIb3kRK8Xjew + GCM_SENDER_ID + 479882132969 + PLIST_VERSION + 1 + BUNDLE_ID + io.flutter.plugins.googleSignInExample + PROJECT_ID + my-flutter-proj + STORAGE_BUCKET + my-flutter-proj.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:479882132969:ios:2643f950e0a0da08 + DATABASE_URL + https://my-flutter-proj.firebaseio.com + SERVER_CLIENT_ID + YOUR_SERVER_CLIENT_ID + + \ No newline at end of file diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Info.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..187584d1cfd9 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Info.plist @@ -0,0 +1,64 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Google Sign-In Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + GoogleSignInExample + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + com.googleusercontent.apps.479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + + diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/main.m b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/main.m new file mode 100644 index 000000000000..f143297b30d6 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/main.m @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "AppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/packages/google_sign_in/google_sign_in/example/ios/RunnerTests/GoogleSignInTests.m b/packages/google_sign_in/google_sign_in_ios/example/ios/RunnerTests/GoogleSignInTests.m similarity index 93% rename from packages/google_sign_in/google_sign_in/example/ios/RunnerTests/GoogleSignInTests.m rename to packages/google_sign_in/google_sign_in_ios/example/ios/RunnerTests/GoogleSignInTests.m index 6f8b821a5299..3bc08d18604a 100644 --- a/packages/google_sign_in/google_sign_in/example/ios/RunnerTests/GoogleSignInTests.m +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/RunnerTests/GoogleSignInTests.m @@ -5,8 +5,8 @@ @import Flutter; @import XCTest; -@import google_sign_in; -@import google_sign_in.Test; +@import google_sign_in_ios; +@import google_sign_in_ios.Test; @import GoogleSignIn; // OCMock library doesn't generate a valid modulemap. @@ -73,35 +73,8 @@ - (void)testDisconnect { OCMVerify([self.mockSignIn disconnect]); } -- (void)testClearAuthCache { - FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"clearAuthCache" - arguments:nil]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(id result) { - XCTAssertNil(result); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - #pragma mark - Init -- (void)testInitGamesSignInUnsupported { - FlutterMethodCall *methodCall = - [FlutterMethodCall methodCallWithMethodName:@"init" - arguments:@{@"signInOption" : @"SignInOption.games"}]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(FlutterError *result) { - XCTAssertEqualObjects(result.code, @"unsupported-options"); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - - (void)testInitGoogleServiceInfoPlist { FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"init" diff --git a/packages/google_sign_in/google_sign_in/example/ios/RunnerUITests/Info.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/RunnerTests/Info.plist similarity index 100% rename from packages/google_sign_in/google_sign_in/example/ios/RunnerUITests/Info.plist rename to packages/google_sign_in/google_sign_in_ios/example/ios/RunnerTests/Info.plist diff --git a/packages/google_sign_in/google_sign_in/example/ios/RunnerUITests/GoogleSignInUITests.m b/packages/google_sign_in/google_sign_in_ios/example/ios/RunnerUITests/GoogleSignInUITests.m similarity index 100% rename from packages/google_sign_in/google_sign_in/example/ios/RunnerUITests/GoogleSignInUITests.m rename to packages/google_sign_in/google_sign_in_ios/example/ios/RunnerUITests/GoogleSignInUITests.m diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/Info.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/RunnerUITests/Info.plist similarity index 100% rename from packages/image_picker/image_picker/example/ios/RunnerTests/Info.plist rename to packages/google_sign_in/google_sign_in_ios/example/ios/RunnerUITests/Info.plist diff --git a/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart b/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart new file mode 100644 index 000000000000..a750c330001d --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart @@ -0,0 +1,179 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:convert' show json; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:http/http.dart' as http; + +void main() { + runApp( + MaterialApp( + title: 'Google Sign In', + home: SignInDemo(), + ), + ); +} + +class SignInDemo extends StatefulWidget { + @override + State createState() => SignInDemoState(); +} + +class SignInDemoState extends State { + GoogleSignInUserData? _currentUser; + String _contactText = ''; + // Future that completes when `init` has completed on the sign in instance. + Future? _initialization; + + @override + void initState() { + super.initState(); + _signIn(); + } + + Future _ensureInitialized() { + return _initialization ??= GoogleSignInPlatform.instance.init( + scopes: [ + 'email', + 'https://www.googleapis.com/auth/contacts.readonly', + ], + )..catchError((dynamic _) { + _initialization = null; + }); + } + + void _setUser(GoogleSignInUserData? user) { + setState(() { + _currentUser = user; + if (user != null) { + _handleGetContact(user); + } + }); + } + + Future _signIn() async { + await _ensureInitialized(); + final GoogleSignInUserData? newUser = + await GoogleSignInPlatform.instance.signInSilently(); + _setUser(newUser); + } + + Future> _getAuthHeaders() async { + final GoogleSignInUserData? user = _currentUser; + if (user == null) { + throw StateError('No user signed in'); + } + + final GoogleSignInTokenData response = + await GoogleSignInPlatform.instance.getTokens( + email: user.email, + shouldRecoverAuth: true, + ); + + return { + 'Authorization': 'Bearer ${response.accessToken}', + // TODO(kevmoo): Use the correct value once it's available. + // See https://github.com/flutter/flutter/issues/80905 + 'X-Goog-AuthUser': '0', + }; + } + + Future _handleGetContact(GoogleSignInUserData user) async { + setState(() { + _contactText = 'Loading contact info...'; + }); + final http.Response response = await http.get( + Uri.parse('https://people.googleapis.com/v1/people/me/connections' + '?requestMask.includeField=person.names'), + headers: await _getAuthHeaders(), + ); + if (response.statusCode != 200) { + setState(() { + _contactText = 'People API gave a ${response.statusCode} ' + 'response. Check logs for details.'; + }); + print('People API ${response.statusCode} response: ${response.body}'); + return; + } + final Map data = + json.decode(response.body) as Map; + final int contactCount = + (data['connections'] as List?)?.length ?? 0; + setState(() { + _contactText = '$contactCount contacts found'; + }); + } + + Future _handleSignIn() async { + try { + await _ensureInitialized(); + _setUser(await GoogleSignInPlatform.instance.signIn()); + } catch (error) { + final bool canceled = + error is PlatformException && error.code == 'sign_in_canceled'; + if (!canceled) { + print(error); + } + } + } + + Future _handleSignOut() async { + await _ensureInitialized(); + await GoogleSignInPlatform.instance.disconnect(); + } + + Widget _buildBody() { + final GoogleSignInUserData? user = _currentUser; + if (user != null) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ListTile( + title: Text(user.displayName ?? ''), + subtitle: Text(user.email), + ), + const Text('Signed in successfully.'), + Text(_contactText), + ElevatedButton( + child: const Text('SIGN OUT'), + onPressed: _handleSignOut, + ), + ElevatedButton( + child: const Text('REFRESH'), + onPressed: () => _handleGetContact(user), + ), + ], + ); + } else { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const Text('You are not currently signed in.'), + ElevatedButton( + child: const Text('SIGN IN'), + onPressed: _handleSignIn, + ), + ], + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Google Sign In'), + ), + body: ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: _buildBody(), + )); + } +} diff --git a/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml new file mode 100644 index 000000000000..f2d32c521c1a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: google_sign_in_example +description: Example of Google Sign-In plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + google_sign_in_ios: + # When depending on this package from a real application you should use: + # google_sign_in_ios: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + google_sign_in_platform_interface: ^2.1.0 + http: ^0.13.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/google_sign_in/google_sign_in_ios/example/test_driver/integration_test.dart b/packages/google_sign_in/google_sign_in_ios/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/google_sign_in/google_sign_in/ios/Assets/.gitkeep b/packages/google_sign_in/google_sign_in_ios/ios/Assets/.gitkeep similarity index 100% rename from packages/google_sign_in/google_sign_in/ios/Assets/.gitkeep rename to packages/google_sign_in/google_sign_in_ios/ios/Assets/.gitkeep diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.h b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.h similarity index 100% rename from packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.h rename to packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.h diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.m b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.m similarity index 85% rename from packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.m rename to packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.m index d13d64d2ba04..5ad69e2ad052 100644 --- a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.m +++ b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.m @@ -50,7 +50,7 @@ @implementation FLTGoogleSignInPlugin { + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/google_sign_in" + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/google_sign_in_ios" binaryMessenger:[registrar messenger]]; FLTGoogleSignInPlugin *instance = [[FLTGoogleSignInPlugin alloc] init]; [registrar addApplicationDelegate:instance]; @@ -78,38 +78,30 @@ - (instancetype)initWithSignIn:(GIDSignIn *)signIn { - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { if ([call.method isEqualToString:@"init"]) { - NSString *signInOption = call.arguments[@"signInOption"]; - if ([signInOption isEqualToString:@"SignInOption.games"]) { - result([FlutterError errorWithCode:@"unsupported-options" - message:@"Games sign in is not supported on iOS" - details:nil]); - } else { - NSString *path = [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" - ofType:@"plist"]; - if (path) { - NSMutableDictionary *plist = - [[NSMutableDictionary alloc] initWithContentsOfFile:path]; - BOOL hasDynamicClientId = [call.arguments[@"clientId"] isKindOfClass:[NSString class]]; - - if (hasDynamicClientId) { - self.signIn.clientID = call.arguments[@"clientId"]; - } else { - self.signIn.clientID = plist[kClientIdKey]; - } + NSString *path = [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" ofType:@"plist"]; + if (path) { + NSMutableDictionary *plist = + [[NSMutableDictionary alloc] initWithContentsOfFile:path]; + BOOL hasDynamicClientId = [call.arguments[@"clientId"] isKindOfClass:[NSString class]]; + + if (hasDynamicClientId) { + self.signIn.clientID = call.arguments[@"clientId"]; + } else { + self.signIn.clientID = plist[kClientIdKey]; + } - self.signIn.serverClientID = plist[kServerClientIdKey]; - self.signIn.scopes = call.arguments[@"scopes"]; - if (call.arguments[@"hostedDomain"] == [NSNull null]) { - self.signIn.hostedDomain = nil; - } else { - self.signIn.hostedDomain = call.arguments[@"hostedDomain"]; - } - result(nil); + self.signIn.serverClientID = plist[kServerClientIdKey]; + self.signIn.scopes = call.arguments[@"scopes"]; + if (call.arguments[@"hostedDomain"] == [NSNull null]) { + self.signIn.hostedDomain = nil; } else { - result([FlutterError errorWithCode:@"missing-config" - message:@"GoogleService-Info.plist file not found" - details:nil]); + self.signIn.hostedDomain = call.arguments[@"hostedDomain"]; } + result(nil); + } else { + result([FlutterError errorWithCode:@"missing-config" + message:@"GoogleService-Info.plist file not found" + details:nil]); } } else if ([call.method isEqualToString:@"signInSilently"]) { if ([self setAccountRequest:result]) { @@ -144,10 +136,6 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result if ([self setAccountRequest:result]) { [self.signIn disconnect]; } - } else if ([call.method isEqualToString:@"clearAuthCache"]) { - // There's nothing to be done here on iOS since the expired/invalid - // tokens are refreshed automatically by getTokensWithHandler. - result(nil); } else if ([call.method isEqualToString:@"requestScopes"]) { GIDGoogleUser *user = self.signIn.currentUser; if (user == nil) { diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.modulemap b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.modulemap similarity index 55% rename from packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.modulemap rename to packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.modulemap index 271f509e7fd7..31e30d93c582 100644 --- a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.modulemap +++ b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.modulemap @@ -1,5 +1,5 @@ -framework module google_sign_in { - umbrella header "google_sign_in-umbrella.h" +framework module google_sign_in_ios { + umbrella header "google_sign_in_ios-umbrella.h" export * module * { export * } diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin_Test.h b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin_Test.h similarity index 89% rename from packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin_Test.h rename to packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin_Test.h index 8fa6cf348018..f8d5be6f8522 100644 --- a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin_Test.h +++ b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin_Test.h @@ -4,7 +4,7 @@ // This header is available in the Test module. Import via "@import google_sign_in.Test;" -#import +#import @class GIDSignIn; diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/google_sign_in-umbrella.h b/packages/google_sign_in/google_sign_in_ios/ios/Classes/google_sign_in_ios-umbrella.h similarity index 85% rename from packages/google_sign_in/google_sign_in/ios/Classes/google_sign_in-umbrella.h rename to packages/google_sign_in/google_sign_in_ios/ios/Classes/google_sign_in_ios-umbrella.h index 343c390f1782..23b7e992a5cd 100644 --- a/packages/google_sign_in/google_sign_in/ios/Classes/google_sign_in-umbrella.h +++ b/packages/google_sign_in/google_sign_in_ios/ios/Classes/google_sign_in_ios-umbrella.h @@ -3,7 +3,7 @@ // found in the LICENSE file. #import -#import +#import FOUNDATION_EXPORT double google_sign_inVersionNumber; FOUNDATION_EXPORT const unsigned char google_sign_inVersionString[]; diff --git a/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec b/packages/google_sign_in/google_sign_in_ios/ios/google_sign_in_ios.podspec similarity index 90% rename from packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec rename to packages/google_sign_in/google_sign_in_ios/ios/google_sign_in_ios.podspec index 19ea753520a7..f583f6cffbf0 100644 --- a/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec +++ b/packages/google_sign_in/google_sign_in_ios/ios/google_sign_in_ios.podspec @@ -2,7 +2,7 @@ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| - s.name = 'google_sign_in' + s.name = 'google_sign_in_ios' s.version = '0.0.1' s.summary = 'Google Sign-In plugin for Flutter' s.description = <<-DESC @@ -11,7 +11,7 @@ Enables Google Sign-In in Flutter apps. s.homepage = 'https://github.com/flutter/plugins/tree/main/packages/google_sign_in' s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/google_sign_in' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_ios' } s.source_files = 'Classes/**/*.{h,m}' s.public_header_files = 'Classes/**/*.h' s.module_map = 'Classes/FLTGoogleSignInPlugin.modulemap' diff --git a/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart b/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart new file mode 100644 index 000000000000..ce8865664507 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart @@ -0,0 +1,97 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:flutter/services.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; + +import 'src/utils.dart'; + +/// iOS implementation of [GoogleSignInPlatform]. +class GoogleSignInIOS extends GoogleSignInPlatform { + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + @visibleForTesting + MethodChannel channel = + const MethodChannel('plugins.flutter.io/google_sign_in_ios'); + + /// Registers this class as the default instance of [GoogleSignInPlatform]. + static void registerWith() { + GoogleSignInPlatform.instance = GoogleSignInIOS(); + } + + @override + Future init({ + List scopes = const [], + SignInOption signInOption = SignInOption.standard, + String? hostedDomain, + String? clientId, + }) { + if (signInOption == SignInOption.games) { + throw PlatformException( + code: 'unsupported-options', + message: 'Games sign in is not supported on iOS'); + } + return channel.invokeMethod('init', { + 'scopes': scopes, + 'hostedDomain': hostedDomain, + 'clientId': clientId, + }); + } + + @override + Future signInSilently() { + return channel + .invokeMapMethod('signInSilently') + .then(getUserDataFromMap); + } + + @override + Future signIn() { + return channel + .invokeMapMethod('signIn') + .then(getUserDataFromMap); + } + + @override + Future getTokens( + {required String email, bool? shouldRecoverAuth = true}) { + return channel + .invokeMapMethod('getTokens', { + 'email': email, + 'shouldRecoverAuth': shouldRecoverAuth, + }).then((Map? result) => getTokenDataFromMap(result!)); + } + + @override + Future signOut() { + return channel.invokeMapMethod('signOut'); + } + + @override + Future disconnect() { + return channel.invokeMapMethod('disconnect'); + } + + @override + Future isSignedIn() async { + return (await channel.invokeMethod('isSignedIn'))!; + } + + @override + Future clearAuthCache({String? token}) async { + // There's nothing to be done here on iOS since the expired/invalid + // tokens are refreshed automatically by getTokens. + } + + @override + Future requestScopes(List scopes) async { + return (await channel.invokeMethod( + 'requestScopes', + >{'scopes': scopes}, + ))!; + } +} diff --git a/packages/google_sign_in/google_sign_in_ios/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_ios/lib/src/utils.dart new file mode 100644 index 000000000000..5cd7c20b829a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/lib/src/utils.dart @@ -0,0 +1,28 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; + +/// Converts user data coming from native code into the proper platform interface type. +GoogleSignInUserData? getUserDataFromMap(Map? data) { + if (data == null) { + return null; + } + return GoogleSignInUserData( + email: data['email']! as String, + id: data['id']! as String, + displayName: data['displayName'] as String?, + photoUrl: data['photoUrl'] as String?, + idToken: data['idToken'] as String?, + serverAuthCode: data['serverAuthCode'] as String?); +} + +/// Converts token data coming from native code into the proper platform interface type. +GoogleSignInTokenData getTokenDataFromMap(Map data) { + return GoogleSignInTokenData( + idToken: data['idToken'] as String?, + accessToken: data['accessToken'] as String?, + serverAuthCode: data['serverAuthCode'] as String?, + ); +} diff --git a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml new file mode 100644 index 000000000000..e5ef3832ff0e --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml @@ -0,0 +1,36 @@ +name: google_sign_in_ios +description: Android implementation of the google_sign_in plugin. +repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in_ios +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 +version: 5.2.6 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: google_sign_in + platforms: + ios: + dartPluginClass: GoogleSignInIOS + pluginClass: FLTGoogleSignInPlugin + +dependencies: + flutter: + sdk: flutter + google_sign_in_platform_interface: ^2.1.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +# The example deliberately includes limited-use secrets. +false_secrets: + - /example/ios/Runner/GoogleService-Info.plist + - /example/ios/RunnerTests/GoogleSignInTests.m + - /example/lib/main.dart diff --git a/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart b/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart new file mode 100644 index 000000000000..92637e938fd9 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart @@ -0,0 +1,151 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_ios/google_sign_in_ios.dart'; +import 'package:google_sign_in_ios/src/utils.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; + +const Map kUserData = { + 'email': 'john.doe@gmail.com', + 'id': '8162538176523816253123', + 'photoUrl': 'https://lh5.googleusercontent.com/photo.jpg', + 'displayName': 'John Doe', + 'idToken': '123', + 'serverAuthCode': '789', +}; + +const Map kTokenData = { + 'idToken': '123', + 'accessToken': '456', + 'serverAuthCode': '789', +}; + +const Map kDefaultResponses = { + 'init': null, + 'signInSilently': kUserData, + 'signIn': kUserData, + 'signOut': null, + 'disconnect': null, + 'isSignedIn': true, + 'getTokens': kTokenData, + 'requestScopes': true, +}; + +final GoogleSignInUserData? kUser = getUserDataFromMap(kUserData); +final GoogleSignInTokenData kToken = + getTokenDataFromMap(kTokenData as Map); + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final GoogleSignInIOS googleSignIn = GoogleSignInIOS(); + final MethodChannel channel = googleSignIn.channel; + + late List log; + late Map + responses; // Some tests mutate some kDefaultResponses + + setUp(() { + responses = Map.from(kDefaultResponses); + log = []; + channel.setMockMethodCallHandler((MethodCall methodCall) { + log.add(methodCall); + final dynamic response = responses[methodCall.method]; + if (response != null && response is Exception) { + return Future.error('$response'); + } + return Future.value(response); + }); + }); + + test('registered instance', () { + GoogleSignInIOS.registerWith(); + expect(GoogleSignInPlatform.instance, isA()); + }); + + test('init throws for SignInOptions.games', () async { + expect( + () => googleSignIn.init( + hostedDomain: 'example.com', + signInOption: SignInOption.games, + clientId: 'fakeClientId'), + throwsA(isInstanceOf().having( + (PlatformException e) => e.code, 'code', 'unsupported-options'))); + }); + + test('signInSilently transforms platform data to GoogleSignInUserData', + () async { + final dynamic response = await googleSignIn.signInSilently(); + expect(response, kUser); + }); + test('signInSilently Exceptions -> throws', () async { + responses['signInSilently'] = Exception('Not a user'); + expect(googleSignIn.signInSilently(), + throwsA(isInstanceOf())); + }); + + test('signIn transforms platform data to GoogleSignInUserData', () async { + final dynamic response = await googleSignIn.signIn(); + expect(response, kUser); + }); + test('signIn Exceptions -> throws', () async { + responses['signIn'] = Exception('Not a user'); + expect(googleSignIn.signIn(), throwsA(isInstanceOf())); + }); + + test('getTokens transforms platform data to GoogleSignInTokenData', () async { + final dynamic response = await googleSignIn.getTokens( + email: 'example@example.com', shouldRecoverAuth: false); + expect(response, kToken); + expect( + log[0], + isMethodCall('getTokens', arguments: { + 'email': 'example@example.com', + 'shouldRecoverAuth': false, + })); + }); + + test('clearAuthCache is a no-op', () async { + await googleSignIn.clearAuthCache(token: 'abc'); + expect(log.isEmpty, true); + }); + + test('Other functions pass through arguments to the channel', () async { + final Map tests = { + () { + googleSignIn.init( + hostedDomain: 'example.com', + scopes: ['two', 'scopes'], + clientId: 'fakeClientId'); + }: isMethodCall('init', arguments: { + 'hostedDomain': 'example.com', + 'scopes': ['two', 'scopes'], + 'clientId': 'fakeClientId', + }), + () { + googleSignIn.getTokens( + email: 'example@example.com', shouldRecoverAuth: false); + }: isMethodCall('getTokens', arguments: { + 'email': 'example@example.com', + 'shouldRecoverAuth': false, + }), + () { + googleSignIn.requestScopes(['newScope', 'anotherScope']); + }: isMethodCall('requestScopes', arguments: { + 'scopes': ['newScope', 'anotherScope'], + }), + googleSignIn.signOut: isMethodCall('signOut', arguments: null), + googleSignIn.disconnect: isMethodCall('disconnect', arguments: null), + googleSignIn.isSignedIn: isMethodCall('isSignedIn', arguments: null), + }; + + for (final Function f in tests.keys) { + f(); + } + + expect(log, tests.values); + }); +} diff --git a/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md b/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md index 66fdb3e72a56..da214d3ce6a9 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Removes unnecessary imports. + ## 2.1.2 * Internal code cleanup for stricter analysis options. diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart index 1abda09fa99f..e56d2028a205 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart @@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/services.dart'; import '../google_sign_in_platform_interface.dart'; -import 'types.dart'; import 'utils.dart'; /// An implementation of [GoogleSignInPlatform] that uses method channels. diff --git a/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart b/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart index a1d83c3f05e6..b6604d1e658e 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart @@ -5,7 +5,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; -import 'package:google_sign_in_platform_interface/src/types.dart'; import 'package:google_sign_in_platform_interface/src/utils.dart'; const Map kUserData = { diff --git a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md index e35caf75ea1f..aab4acae0376 100644 --- a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md @@ -1,6 +1,8 @@ -## NEXT +## 0.10.1 * Updates minimum Flutter version to 2.8. +* Passes `plugin_name` to Google Sign-In's `init` method so new applications can + continue using this plugin after April 30th 2022. Issue [#88084](https://github.com/flutter/flutter/issues/88084). ## 0.10.0+5 diff --git a/packages/google_sign_in/google_sign_in_web/README.md b/packages/google_sign_in/google_sign_in_web/README.md index 4ee1a2956b45..463603e73e54 100644 --- a/packages/google_sign_in/google_sign_in_web/README.md +++ b/packages/google_sign_in/google_sign_in_web/README.md @@ -37,7 +37,7 @@ Normally `flutter run` starts in a random port. In the case where you need to de You can tell `flutter run` to listen for requests in a specific host and port with the following: -``` +```sh flutter run -d chrome --web-hostname localhost --web-port 7357 ``` diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart index 2af9476dbb33..9db024361580 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:html' as html; + import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; @@ -126,6 +128,25 @@ void main() { throwsAssertionError); }); + // See: https://github.com/flutter/flutter/issues/88084 + testWidgets('Init passes plugin_name parameter with the expected value', + (WidgetTester tester) async { + await plugin.init( + hostedDomain: 'foo', + scopes: ['some', 'scope'], + clientId: '1234', + ); + + final Object? initParameters = + js_util.getProperty(html.window, 'gapi2.init.parameters'); + expect(initParameters, isNotNull); + + final Object? pluginNameParameter = + js_util.getProperty(initParameters!, 'plugin_name'); + expect(pluginNameParameter, isA()); + expect(pluginNameParameter, 'dart-google_sign_in_web'); + }); + group('Successful .init, then', () { setUp(() async { await plugin.init( diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart index cc49b2759e7f..84f4e6ee8ba8 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart @@ -12,6 +12,8 @@ var mockUser = ${googleUser(userData)}; function GapiAuth2() {} GapiAuth2.prototype.init = function (initOptions) { + /*Leak the initOptions so we can look at them later.*/ + window['gapi2.init.parameters'] = initOptions; return { then: (onSuccess, onError) => { window.setTimeout(() => { diff --git a/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart index 731ced5ddbbe..533c353df310 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart @@ -92,6 +92,7 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { // The js lib wants a space-separated list of values scope: scopes.join(' '), client_id: appClientId!, + plugin_name: 'dart-google_sign_in_web', )); final Completer isAuthInitialized = Completer(); diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart index 88d196bad007..8e23713c90e9 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart @@ -233,15 +233,23 @@ abstract class ClientConfig { /// The default redirect_uri is the current URL stripped of query parameters and hash fragment. external String? get redirect_uri; external set redirect_uri(String? v); - external factory ClientConfig( - {String client_id, - String cookie_policy, - String scope, - bool fetch_basic_profile, - String? hosted_domain, - String openid_realm, - String /*'popup'|'redirect'*/ ux_mode, - String redirect_uri}); + + /// Allows newly created Client IDs to use the Google Platform Library from now until the March 30th, 2023 deprecation date. + /// See: https://github.com/flutter/flutter/issues/88084 + external String? get plugin_name; + external set plugin_name(String? v); + + external factory ClientConfig({ + String client_id, + String cookie_policy, + String scope, + bool fetch_basic_profile, + String? hosted_domain, + String openid_realm, + String /*'popup'|'redirect'*/ ux_mode, + String redirect_uri, + String plugin_name, + }); } @JS('gapi.auth2.SigninOptionsBuilder') diff --git a/packages/google_sign_in/google_sign_in_web/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/pubspec.yaml index d97a7c4da6f1..5a09453b8e86 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android, iOS and Web. repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 0.10.0+5 +version: 0.10.1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 2ba5a2ce4ce7..f1bf54c5cd35 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.8.5 + +* Moves Android and iOS implementations to federated packages. +* Adds OS version support information to README. + ## 0.8.4+11 * Fixes Activity leak. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index 46a7795b748a..2fa20be34859 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -5,6 +5,10 @@ A Flutter plugin for iOS and Android for picking images from the image library, and taking new pictures with the camera. +| | Android | iOS | Web | +|-------------|---------|--------|----------------------------------| +| **Support** | SDK 21+ | iOS 9+ | [See `image_picker_for_web `][1] | + ## Installation First, add `image_picker` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). @@ -26,9 +30,9 @@ Add the following keys to your _Info.plist_ file, located in `/ios Starting with version **0.8.1** the Android implementation support to pick (multiple) images on Android 4.3 or higher. -No configuration required - the plugin should work out of the box. It is +No configuration required - the plugin should work out of the box. It is however highly recommended to prepare for Android killing the application when -low on memory. How to prepare for this is discussed in the [Handling +low on memory. How to prepare for this is discussed in the [Handling MainActivity destruction on Android](#handling-mainactivity-destruction-on-android) section. @@ -60,12 +64,12 @@ import 'package:image_picker/image_picker.dart'; ### Handling MainActivity destruction on Android When under high memory pressure the Android system may kill the MainActivity of -the application using the image_picker. On Android the image_picker makes use -of the default `Intent.ACTION_GET_CONTENT` or `MediaStore.ACTION_IMAGE_CAPTURE` -intents. This means that while the intent is executing the source application +the application using the image_picker. On Android the image_picker makes use +of the default `Intent.ACTION_GET_CONTENT` or `MediaStore.ACTION_IMAGE_CAPTURE` +intents. This means that while the intent is executing the source application is moved to the background and becomes eligable for cleanup when the system is -low on memory. When the intent finishes executing, Android will restart the -application. Since the data is never returned to the original call use the +low on memory. When the intent finishes executing, Android will restart the +application. Since the data is never returned to the original call use the `ImagePicker.retrieveLostData()` method to retrieve the lost data. For example: ```dart @@ -85,9 +89,9 @@ Future getLostData() async { } ``` -This check should always be run at startup in order to detect and handle this -case. Please refer to the -[example app](https://pub.dev/packages/image_picker/example) for a more +This check should always be run at startup in order to detect and handle this +case. Please refer to the +[example app](https://pub.dev/packages/image_picker/example) for a more complete example of handling this flow. ## Migrating to 0.8.2+ @@ -101,4 +105,6 @@ Starting with version **0.8.2** of the image_picker plugin, new methods have bee | `PickedFile image = await _picker.getImage(...)` | `XFile image = await _picker.pickImage(...)` | | `List images = await _picker.getMultiImage(...)` | `List images = await _picker.pickMultiImage(...)` | | `PickedFile video = await _picker.getVideo(...)` | `XFile video = await _picker.pickVideo(...)` | -| `LostData response = await _picker.getLostData()` | `LostDataResponse response = await _picker.retrieveLostData()` | \ No newline at end of file +| `LostData response = await _picker.getLostData()` | `LostDataResponse response = await _picker.retrieveLostData()` | + +[1]: https://pub.dev/packages/image_picker_for_web#limitations-on-the-web-platform diff --git a/packages/image_picker/image_picker/android/settings.gradle b/packages/image_picker/image_picker/android/settings.gradle deleted file mode 100755 index 5b9496172108..000000000000 --- a/packages/image_picker/image_picker/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'imagepicker' diff --git a/packages/image_picker/image_picker/example/ios/Podfile b/packages/image_picker/image_picker/example/ios/Podfile index 5bc7b7e85717..f7d6a5e68c3a 100644 --- a/packages/image_picker/image_picker/example/ios/Podfile +++ b/packages/image_picker/image_picker/example/ios/Podfile @@ -29,13 +29,6 @@ flutter_ios_podfile_setup target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - - target 'RunnerTests' do - platform :ios, '9.0' - inherit! :search_paths - # Pods for testing - pod 'OCMock', '~> 3.8.1' - end end post_install do |installer| diff --git a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj index 2847bfd85046..589858f39019 100644 --- a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,53 +3,20 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 46; objects = { /* Begin PBXBuildFile section */ - 334733FC266813EE00DCC49E /* ImageUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */; }; - 334733FD266813F100DCC49E /* MetaDataUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 680049252280D736006DD6AB /* MetaDataUtilTests.m */; }; - 334733FE266813F400DCC49E /* PhotoAssetUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */; }; - 334733FF266813FA00DCC49E /* ImagePickerTestImages.m in Sources */ = {isa = PBXBuildFile; fileRef = F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */; }; - 33473400266813FD00DCC49E /* ImagePickerPluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */; }; - 3A72BAD3FAE6E0FA9D80826B /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 35AE65F25E0B8C8214D8372B /* libPods-RunnerTests.a */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 5C9513011EC38BD300040975 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C9513001EC38BD300040975 /* GeneratedPluginRegistrant.m */; }; - 680049382280F2B9006DD6AB /* pngImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 680049352280F2B8006DD6AB /* pngImage.png */; }; - 680049392280F2B9006DD6AB /* jpgImage.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 680049362280F2B8006DD6AB /* jpgImage.jpg */; }; - 6801C8392555D726009DAF8D /* ImagePickerFromGalleryUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6801C8382555D726009DAF8D /* ImagePickerFromGalleryUITests.m */; }; - 86430DF9272D71E9002D9D6C /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; - 86E9A893272754860017E6E0 /* PickerSaveImageToPathOperationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 86E9A892272754860017E6E0 /* PickerSaveImageToPathOperationTests.m */; }; - 86E9A894272754A30017E6E0 /* webpImage.webp in Resources */ = {isa = PBXBuildFile; fileRef = 86E9A88F272747B90017E6E0 /* webpImage.webp */; }; - 86E9A895272769130017E6E0 /* pngImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 680049352280F2B8006DD6AB /* pngImage.png */; }; - 86E9A896272769150017E6E0 /* jpgImage.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 680049362280F2B8006DD6AB /* jpgImage.jpg */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 9FC8F0E9229FA49E00C8D58F /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; - 9FC8F0EC229FA68500C8D58F /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; - BE6173D826A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */; }; F4F7A436CCA4BF276270A3AE /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EC32F6993F4529982D9519F1 /* libPods-Runner.a */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - 334733F72668136400DCC49E /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; - 6801C83B2555D726009DAF8D /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -65,27 +32,18 @@ /* Begin PBXFileReference section */ 0C7B151765FD4249454C49AD /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - 334733F22668136400DCC49E /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 334733F62668136400DCC49E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35AE65F25E0B8C8214D8372B /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 5A9D31B91557877A0E8EF3E7 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 5C9512FF1EC38BD300040975 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 5C9513001EC38BD300040975 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 680049252280D736006DD6AB /* MetaDataUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MetaDataUtilTests.m; sourceTree = ""; }; 680049352280F2B8006DD6AB /* pngImage.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = pngImage.png; sourceTree = ""; }; 680049362280F2B8006DD6AB /* jpgImage.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = jpgImage.jpg; sourceTree = ""; }; 6801632E632668F4349764C9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 6801C8362555D726009DAF8D /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 6801C8382555D726009DAF8D /* ImagePickerFromGalleryUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerFromGalleryUITests.m; sourceTree = ""; }; - 6801C83A2555D726009DAF8D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ImagePickerPluginTests.m; sourceTree = ""; }; - 68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PhotoAssetUtilTests.m; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 86E9A88F272747B90017E6E0 /* webpImage.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = webpImage.webp; sourceTree = ""; }; - 86E9A892272754860017E6E0 /* PickerSaveImageToPathOperationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PickerSaveImageToPathOperationTests.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -95,30 +53,11 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = gifImage.gif; sourceTree = ""; }; - 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImageUtilTests.m; sourceTree = ""; }; - BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerFromLimitedGalleryUITests.m; sourceTree = ""; }; DC6FCAAD4E7580C9B3C2E21D /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; EC32F6993F4529982D9519F1 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - F78AF3172342D9D7008449C7 /* ImagePickerTestImages.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ImagePickerTestImages.h; sourceTree = ""; }; - F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerTestImages.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 334733EF2668136400DCC49E /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 3A72BAD3FAE6E0FA9D80826B /* libPods-RunnerTests.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 6801C8332555D726009DAF8D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -130,21 +69,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 334733F32668136400DCC49E /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */, - 680049252280D736006DD6AB /* MetaDataUtilTests.m */, - 68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */, - F78AF3172342D9D7008449C7 /* ImagePickerTestImages.h */, - F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */, - 68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */, - 86E9A892272754860017E6E0 /* PickerSaveImageToPathOperationTests.m */, - 334733F62668136400DCC49E /* Info.plist */, - ); - path = RunnerTests; - sourceTree = ""; - }; 680049282280E33D006DD6AB /* TestImages */ = { isa = PBXGroup; children = ( @@ -156,16 +80,6 @@ path = TestImages; sourceTree = ""; }; - 6801C8372555D726009DAF8D /* RunnerUITests */ = { - isa = PBXGroup; - children = ( - BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */, - 6801C8382555D726009DAF8D /* ImagePickerFromGalleryUITests.m */, - 6801C83A2555D726009DAF8D /* Info.plist */, - ); - path = RunnerUITests; - sourceTree = ""; - }; 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { isa = PBXGroup; children = ( @@ -194,8 +108,6 @@ 680049282280E33D006DD6AB /* TestImages */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, - 334733F32668136400DCC49E /* RunnerTests */, - 6801C8372555D726009DAF8D /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, 840012C8B5EDBCF56B0E4AC1 /* Pods */, CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, @@ -206,8 +118,6 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - 6801C8362555D726009DAF8D /* RunnerUITests.xctest */, - 334733F22668136400DCC49E /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -248,43 +158,6 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 334733F12668136400DCC49E /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 334733F92668136400DCC49E /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - B8739A4353234497CF76B597 /* [CP] Check Pods Manifest.lock */, - 334733EE2668136400DCC49E /* Sources */, - 334733EF2668136400DCC49E /* Frameworks */, - 334733F02668136400DCC49E /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 334733F82668136400DCC49E /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 334733F22668136400DCC49E /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 6801C8352555D726009DAF8D /* RunnerUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 6801C83F2555D726009DAF8D /* Build configuration list for PBXNativeTarget "RunnerUITests" */; - buildPhases = ( - 6801C8322555D726009DAF8D /* Sources */, - 6801C8332555D726009DAF8D /* Frameworks */, - 6801C8342555D726009DAF8D /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 6801C83C2555D726009DAF8D /* PBXTargetDependency */, - ); - name = RunnerUITests; - productName = RunnerUITests; - productReference = 6801C8362555D726009DAF8D /* RunnerUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; @@ -316,16 +189,6 @@ LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { - 334733F12668136400DCC49E = { - CreatedOnToolsVersion = 12.5; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 6801C8352555D726009DAF8D = { - CreatedOnToolsVersion = 11.7; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; - }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; SystemCapabilities = { @@ -350,34 +213,11 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - 334733F12668136400DCC49E /* RunnerTests */, - 6801C8352555D726009DAF8D /* RunnerUITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 334733F02668136400DCC49E /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 86430DF9272D71E9002D9D6C /* gifImage.gif in Resources */, - 86E9A894272754A30017E6E0 /* webpImage.webp in Resources */, - 86E9A895272769130017E6E0 /* pngImage.png in Resources */, - 86E9A896272769150017E6E0 /* jpgImage.jpg in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 6801C8342555D726009DAF8D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 9FC8F0EC229FA68500C8D58F /* gifImage.gif in Resources */, - 680049382280F2B9006DD6AB /* pngImage.png in Resources */, - 680049392280F2B9006DD6AB /* jpgImage.jpg in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -438,53 +278,9 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - B8739A4353234497CF76B597 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 334733EE2668136400DCC49E /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 334733FD266813F100DCC49E /* MetaDataUtilTests.m in Sources */, - 334733FF266813FA00DCC49E /* ImagePickerTestImages.m in Sources */, - 86E9A893272754860017E6E0 /* PickerSaveImageToPathOperationTests.m in Sources */, - 334733FC266813EE00DCC49E /* ImageUtilTests.m in Sources */, - 33473400266813FD00DCC49E /* ImagePickerPluginTests.m in Sources */, - 334733FE266813F400DCC49E /* PhotoAssetUtilTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 6801C8322555D726009DAF8D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 6801C8392555D726009DAF8D /* ImagePickerFromGalleryUITests.m in Sources */, - BE6173D826A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -497,19 +293,6 @@ }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - 334733F82668136400DCC49E /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 334733F72668136400DCC49E /* PBXContainerItemProxy */; - }; - 6801C83C2555D726009DAF8D /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 6801C83B2555D726009DAF8D /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -530,79 +313,6 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 334733FA2668136400DCC49E /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = DC6FCAAD4E7580C9B3C2E21D /* Pods-RunnerTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Debug; - }; - 334733FB2668136400DCC49E /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 0C7B151765FD4249454C49AD /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Release; - }; - 6801C83D2555D726009DAF8D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = RunnerUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Runner; - }; - name = Debug; - }; - 6801C83E2555D726009DAF8D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = RunnerUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Runner; - }; - name = Release; - }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -754,24 +464,6 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 334733F92668136400DCC49E /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 334733FA2668136400DCC49E /* Debug */, - 334733FB2668136400DCC49E /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 6801C83F2555D726009DAF8D /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 6801C83D2555D726009DAF8D /* Debug */, - 6801C83E2555D726009DAF8D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/packages/image_picker/image_picker/example/pubspec.yaml b/packages/image_picker/image_picker/example/pubspec.yaml index 28b37197d8ff..4fe823587398 100755 --- a/packages/image_picker/image_picker/example/pubspec.yaml +++ b/packages/image_picker/image_picker/example/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: video_player: ^2.1.4 dev_dependencies: - espresso: ^0.1.0+2 + espresso: ^0.2.0 flutter_driver: sdk: flutter integration_test: diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin_Test.h b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin_Test.h deleted file mode 100644 index 5442f7d089c6..000000000000 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin_Test.h +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// This header is available in the Test module. Import via "@import image_picker.Test;" - -#import - -/** Methods exposed for unit testing. */ -@interface FLTImagePickerPlugin () - -/** The Flutter result callback use to report results back to Flutter App. */ -@property(copy, nonatomic) FlutterResult result; - -/** - * Applies NSMutableArray on the FLutterResult. - * - * NSString must be returned by FlutterResult if the single image - * mode is active. It is checked by maxImagesAllowed and - * returns the first object of the pathlist. - * - * NSMutableArray must be returned by FlutterResult if the multi-image - * mode is active. After the pathlist count is checked then it returns - * the pathlist. - * - * @param pathList that should be applied to FlutterResult. - */ -- (void)handleSavedPathList:(NSArray *)pathList; - -/** - * Tells the delegate that the user cancelled the pick operation. - * - * Your delegate’s implementation of this method should dismiss the picker view - * by calling the dismissModalViewControllerAnimated: method of the parent - * view controller. - * - * Implementation of this method is optional, but expected. - * - * @param picker The controller object managing the image picker interface. - */ -- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker; - -/** - * Sets UIImagePickerController instances that will be used when a new - * controller would normally be created. Each call to - * createImagePickerController will remove the current first element from - * the array. - * - * Should be used for testing purposes only. - */ -- (void)setImagePickerControllerOverrides: - (NSArray *)imagePickerControllers; - -@end diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 553599f7306f..77a50916283e 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.4+11 +version: 0.8.5 environment: sdk: ">=2.14.0 <3.0.0" @@ -13,18 +13,18 @@ flutter: plugin: platforms: android: - package: io.flutter.plugins.imagepicker - pluginClass: ImagePickerPlugin + default_package: image_picker_android ios: - pluginClass: FLTImagePickerPlugin + default_package: image_picker_ios web: default_package: image_picker_for_web dependencies: flutter: sdk: flutter - flutter_plugin_android_lifecycle: ^2.0.1 + image_picker_android: ^0.8.4+11 image_picker_for_web: ^2.1.0 + image_picker_ios: ^0.8.4+11 image_picker_platform_interface: ^2.3.0 dev_dependencies: diff --git a/packages/image_picker/image_picker_android/AUTHORS b/packages/image_picker/image_picker_android/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/image_picker/image_picker_android/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md new file mode 100644 index 000000000000..3472ade28d5b --- /dev/null +++ b/packages/image_picker/image_picker_android/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.8.4+11 + +* Splits from `image_picker` as a federated implementation. diff --git a/packages/image_picker/image_picker_android/LICENSE b/packages/image_picker/image_picker_android/LICENSE new file mode 100644 index 000000000000..0be8bbc3e68d --- /dev/null +++ b/packages/image_picker/image_picker_android/LICENSE @@ -0,0 +1,231 @@ +image_picker + +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- +aFileChooser + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2011 - 2013 Paul Burke + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/image_picker/image_picker_android/README.md b/packages/image_picker/image_picker_android/README.md new file mode 100755 index 000000000000..43d08c2a8b3a --- /dev/null +++ b/packages/image_picker/image_picker_android/README.md @@ -0,0 +1,11 @@ +# image\_picker\_android + +The Android implementation of [`image_picker`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `image_picker` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/image_picker +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/image_picker/image_picker/android/build.gradle b/packages/image_picker/image_picker_android/android/build.gradle similarity index 100% rename from packages/image_picker/image_picker/android/build.gradle rename to packages/image_picker/image_picker_android/android/build.gradle diff --git a/packages/image_picker/image_picker_android/android/settings.gradle b/packages/image_picker/image_picker_android/android/settings.gradle new file mode 100755 index 000000000000..3c673efcd542 --- /dev/null +++ b/packages/image_picker/image_picker_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'image_picker_android' diff --git a/packages/image_picker/image_picker/android/src/main/AndroidManifest.xml b/packages/image_picker/image_picker_android/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/image_picker/image_picker/android/src/main/AndroidManifest.xml rename to packages/image_picker/image_picker_android/android/src/main/AndroidManifest.xml diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java similarity index 100% rename from packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java rename to packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java similarity index 100% rename from packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java rename to packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java similarity index 100% rename from packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java rename to packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java similarity index 100% rename from packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java rename to packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerFileProvider.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerFileProvider.java similarity index 100% rename from packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerFileProvider.java rename to packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerFileProvider.java diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java similarity index 100% rename from packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java rename to packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java similarity index 100% rename from packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java rename to packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java similarity index 100% rename from packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java rename to packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java diff --git a/packages/image_picker/image_picker/android/src/main/res/xml/flutter_image_picker_file_paths.xml b/packages/image_picker/image_picker_android/android/src/main/res/xml/flutter_image_picker_file_paths.xml similarity index 100% rename from packages/image_picker/image_picker/android/src/main/res/xml/flutter_image_picker_file_paths.xml rename to packages/image_picker/image_picker_android/android/src/main/res/xml/flutter_image_picker_file_paths.xml diff --git a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java similarity index 100% rename from packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java rename to packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java diff --git a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java similarity index 100% rename from packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java rename to packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java diff --git a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java similarity index 100% rename from packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java rename to packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java diff --git a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java similarity index 100% rename from packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java rename to packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java diff --git a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java similarity index 100% rename from packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java rename to packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java diff --git a/packages/image_picker/image_picker/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/packages/image_picker/image_picker_android/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker similarity index 100% rename from packages/image_picker/image_picker/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker rename to packages/image_picker/image_picker_android/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/packages/image_picker/image_picker/android/src/test/resources/pngImage.png b/packages/image_picker/image_picker_android/android/src/test/resources/pngImage.png similarity index 100% rename from packages/image_picker/image_picker/android/src/test/resources/pngImage.png rename to packages/image_picker/image_picker_android/android/src/test/resources/pngImage.png diff --git a/packages/image_picker/image_picker_android/example/README.md b/packages/image_picker/image_picker_android/example/README.md new file mode 100755 index 000000000000..86d5c23ba209 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/README.md @@ -0,0 +1,8 @@ +# image_picker_example + +Demonstrates how to use the `image_picker` plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/image_picker/image_picker_android/example/android/app/build.gradle b/packages/image_picker/image_picker_android/example/android/app/build.gradle new file mode 100755 index 000000000000..e73e3fe01003 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/build.gradle @@ -0,0 +1,67 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 31 + testOptions.unitTests.includeAndroidResources = true + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "io.flutter.plugins.imagepicker.example" + minSdkVersion 16 + targetSdkVersion 28 + multiDexEnabled true + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } + + testOptions { + unitTests.returnDefaultValues = true + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + api 'androidx.test:core:1.2.0' +} diff --git a/packages/image_picker/image_picker_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/image_picker/image_picker_android/example/android/app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..9a4163a4f5ee --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java new file mode 100644 index 000000000000..91e068fa8043 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.imagepickerexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerTest.java b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerTest.java new file mode 100644 index 000000000000..c4a1532d940c --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerTest.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.imagepickerexample; + +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ActivityScenario; +import io.flutter.plugins.imagepicker.ImagePickerPlugin; +import org.junit.Test; + +public class ImagePickerTest { + @Test + public void imagePickerPluginIsAdded() { + final ActivityScenario scenario = + ActivityScenario.launch(ImagePickerTestActivity.class); + scenario.onActivity( + activity -> { + assertTrue(activity.engine.getPlugins().has(ImagePickerPlugin.class)); + }); + } +} diff --git a/packages/image_picker/image_picker_android/example/android/app/src/debug/AndroidManifest.xml b/packages/image_picker/image_picker_android/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..6f85cefded34 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/AndroidManifest.xml b/packages/image_picker/image_picker_android/example/android/app/src/main/AndroidManifest.xml new file mode 100755 index 000000000000..543fca922e1b --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/.gitignore b/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/.gitignore new file mode 100755 index 000000000000..9eb4563d2ae1 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/.gitignore @@ -0,0 +1 @@ +GeneratedPluginRegistrant.java diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/ImagePickerTestActivity.java b/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/ImagePickerTestActivity.java new file mode 100644 index 000000000000..827687a10e79 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/ImagePickerTestActivity.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.imagepickerexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Makes the FlutterEngine accessible for testing. +public class ImagePickerTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 000000000000..17987b79bb8a Binary files /dev/null and b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100755 index 000000000000..09d4391482be Binary files /dev/null and b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/image_picker/image_picker_android/example/android/build.gradle b/packages/image_picker/image_picker_android/example/android/build.gradle new file mode 100755 index 000000000000..e101ac08df55 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.3.0' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/image_picker/image_picker_android/example/android/gradle.properties b/packages/image_picker/image_picker_android/example/android/gradle.properties new file mode 100755 index 000000000000..6effed032590 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true +android.enableUnitTestBinaryResources=true diff --git a/packages/image_picker/image_picker_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/image_picker/image_picker_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..019065d1d650 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/packages/image_picker/image_picker_android/example/android/settings.gradle b/packages/image_picker/image_picker_android/example/android/settings.gradle new file mode 100755 index 000000000000..115da6cb4f4d --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withInputStream { stream -> plugins.load(stream) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/packages/image_picker/image_picker_android/example/integration_test/image_picker_test.dart b/packages/image_picker/image_picker_android/example/integration_test/image_picker_test.dart new file mode 100644 index 000000000000..2b82b4bda5e4 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/integration_test/image_picker_test.dart @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('placeholder test', (WidgetTester tester) async {}); +} diff --git a/packages/image_picker/image_picker_android/example/lib/main.dart b/packages/image_picker/image_picker_android/example/lib/main.dart new file mode 100755 index 000000000000..48eee35445da --- /dev/null +++ b/packages/image_picker/image_picker_android/example/lib/main.dart @@ -0,0 +1,467 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:video_player/video_player.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Image Picker Demo', + home: MyHomePage(title: 'Image Picker Example'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key, this.title}) : super(key: key); + + final String? title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + List? _imageFileList; + + set _imageFile(XFile? value) { + _imageFileList = value == null ? null : [value]; + } + + dynamic _pickImageError; + bool isVideo = false; + + VideoPlayerController? _controller; + VideoPlayerController? _toBeDisposed; + String? _retrieveDataError; + + final ImagePickerPlatform _picker = ImagePickerPlatform.instance; + final TextEditingController maxWidthController = TextEditingController(); + final TextEditingController maxHeightController = TextEditingController(); + final TextEditingController qualityController = TextEditingController(); + + Future _playVideo(XFile? file) async { + if (file != null && mounted) { + await _disposeVideoController(); + late VideoPlayerController controller; + if (kIsWeb) { + controller = VideoPlayerController.network(file.path); + } else { + controller = VideoPlayerController.file(File(file.path)); + } + _controller = controller; + // In web, most browsers won't honor a programmatic call to .play + // if the video has a sound track (and is not muted). + // Mute the video so it auto-plays in web! + // This is not needed if the call to .play is the result of user + // interaction (clicking on a "play" button, for example). + const double volume = kIsWeb ? 0.0 : 1.0; + await controller.setVolume(volume); + await controller.initialize(); + await controller.setLooping(true); + await controller.play(); + setState(() {}); + } + } + + Future _onImageButtonPressed(ImageSource source, + {BuildContext? context, bool isMultiImage = false}) async { + if (_controller != null) { + await _controller!.setVolume(0.0); + } + if (isVideo) { + final XFile? file = await _picker.getVideo( + source: source, maxDuration: const Duration(seconds: 10)); + await _playVideo(file); + } else if (isMultiImage) { + await _displayPickImageDialog(context!, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List? pickedFileList = await _picker.getMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _imageFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else { + await _displayPickImageDialog(context!, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _imageFile = pickedFile; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } + } + + @override + void deactivate() { + if (_controller != null) { + _controller!.setVolume(0.0); + _controller!.pause(); + } + super.deactivate(); + } + + @override + void dispose() { + _disposeVideoController(); + maxWidthController.dispose(); + maxHeightController.dispose(); + qualityController.dispose(); + super.dispose(); + } + + Future _disposeVideoController() async { + if (_toBeDisposed != null) { + await _toBeDisposed!.dispose(); + } + _toBeDisposed = _controller; + _controller = null; + } + + Widget _previewVideo() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_controller == null) { + return const Text( + 'You have not yet picked a video', + textAlign: TextAlign.center, + ); + } + return Padding( + padding: const EdgeInsets.all(10.0), + child: AspectRatioVideo(_controller), + ); + } + + Widget _previewImages() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_imageFileList != null) { + return Semantics( + child: ListView.builder( + key: UniqueKey(), + itemBuilder: (BuildContext context, int index) { + // Why network for web? + // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform + return Semantics( + label: 'image_picker_example_picked_image', + child: kIsWeb + ? Image.network(_imageFileList![index].path) + : Image.file(File(_imageFileList![index].path)), + ); + }, + itemCount: _imageFileList!.length, + ), + label: 'image_picker_example_picked_images'); + } else if (_pickImageError != null) { + return Text( + 'Pick image error: $_pickImageError', + textAlign: TextAlign.center, + ); + } else { + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + } + } + + Widget _handlePreview() { + if (isVideo) { + return _previewVideo(); + } else { + return _previewImages(); + } + } + + Future retrieveLostData() async { + final LostDataResponse response = await _picker.getLostData(); + if (response.isEmpty) { + return; + } + if (response.file != null) { + if (response.type == RetrieveType.video) { + isVideo = true; + await _playVideo(response.file); + } else { + isVideo = false; + setState(() { + _imageFile = response.file; + _imageFileList = response.files; + }); + } + } else { + _retrieveDataError = response.exception!.code; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title!), + ), + body: Center( + child: !kIsWeb && defaultTargetPlatform == TargetPlatform.android + ? FutureBuilder( + future: retrieveLostData(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + case ConnectionState.done: + return _handlePreview(); + default: + if (snapshot.hasError) { + return Text( + 'Pick image/video error: ${snapshot.error}}', + textAlign: TextAlign.center, + ); + } else { + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + } + } + }, + ) + : _handlePreview(), + ), + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Semantics( + label: 'image_picker_example_from_gallery', + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed(ImageSource.gallery, context: context); + }, + heroTag: 'image0', + tooltip: 'Pick Image from gallery', + child: const Icon(Icons.photo), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + ); + }, + heroTag: 'image1', + tooltip: 'Pick Multiple Image from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'image2', + tooltip: 'Take a Photo', + child: const Icon(Icons.camera_alt), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + isVideo = true; + _onImageButtonPressed(ImageSource.gallery); + }, + heroTag: 'video0', + tooltip: 'Pick Video from gallery', + child: const Icon(Icons.video_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + isVideo = true; + _onImageButtonPressed(ImageSource.camera); + }, + heroTag: 'video1', + tooltip: 'Take a Video', + child: const Icon(Icons.videocam), + ), + ), + ], + ), + ); + } + + Text? _getRetrieveErrorWidget() { + if (_retrieveDataError != null) { + final Text result = Text(_retrieveDataError!); + _retrieveDataError = null; + return result; + } + return null; + } + + Future _displayPickImageDialog( + BuildContext context, OnPickImageCallback onPick) async { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Add optional parameters'), + content: Column( + children: [ + TextField( + controller: maxWidthController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxWidth if desired'), + ), + TextField( + controller: maxHeightController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxHeight if desired'), + ), + TextField( + controller: qualityController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: 'Enter quality if desired'), + ), + ], + ), + actions: [ + TextButton( + child: const Text('CANCEL'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('PICK'), + onPressed: () { + final double? width = maxWidthController.text.isNotEmpty + ? double.parse(maxWidthController.text) + : null; + final double? height = maxHeightController.text.isNotEmpty + ? double.parse(maxHeightController.text) + : null; + final int? quality = qualityController.text.isNotEmpty + ? int.parse(qualityController.text) + : null; + onPick(width, height, quality); + Navigator.of(context).pop(); + }), + ], + ); + }); + } +} + +typedef OnPickImageCallback = void Function( + double? maxWidth, double? maxHeight, int? quality); + +class AspectRatioVideo extends StatefulWidget { + const AspectRatioVideo(this.controller); + + final VideoPlayerController? controller; + + @override + AspectRatioVideoState createState() => AspectRatioVideoState(); +} + +class AspectRatioVideoState extends State { + VideoPlayerController? get controller => widget.controller; + bool initialized = false; + + void _onVideoControllerUpdate() { + if (!mounted) { + return; + } + if (initialized != controller!.value.isInitialized) { + initialized = controller!.value.isInitialized; + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + controller!.addListener(_onVideoControllerUpdate); + } + + @override + void dispose() { + controller!.removeListener(_onVideoControllerUpdate); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (initialized) { + return Center( + child: AspectRatio( + aspectRatio: controller!.value.aspectRatio, + child: VideoPlayer(controller!), + ), + ); + } else { + return Container(); + } + } +} diff --git a/packages/image_picker/image_picker_android/example/pubspec.yaml b/packages/image_picker/image_picker_android/example/pubspec.yaml new file mode 100755 index 000000000000..0d88ae139c71 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/pubspec.yaml @@ -0,0 +1,31 @@ +name: image_picker_example +description: Demonstrates how to use the image_picker plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + flutter_plugin_android_lifecycle: ^2.0.1 + image_picker_android: + # When depending on this package from a real application you should use: + # image_picker_android: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + image_picker_platform_interface: ^2.3.0 + video_player: ^2.1.4 + +dev_dependencies: + espresso: ^0.2.0 + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/image_picker/image_picker_android/example/test_driver/integration_test.dart b/packages/image_picker/image_picker_android/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml new file mode 100755 index 000000000000..dbeef9bed193 --- /dev/null +++ b/packages/image_picker/image_picker_android/pubspec.yaml @@ -0,0 +1,28 @@ +name: image_picker_android +description: Android implementation of the image_picker plugin. +repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/image_picker_android +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 +version: 0.8.4+11 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + implements: image_picker + platforms: + android: + package: io.flutter.plugins.imagepicker + pluginClass: ImagePickerPlugin + +dependencies: + flutter: + sdk: flutter + flutter_plugin_android_lifecycle: ^2.0.1 + image_picker_platform_interface: ^2.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^5.0.0 diff --git a/packages/image_picker/image_picker_ios/AUTHORS b/packages/image_picker/image_picker_ios/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/image_picker/image_picker_ios/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/image_picker/image_picker_ios/CHANGELOG.md b/packages/image_picker/image_picker_ios/CHANGELOG.md new file mode 100644 index 000000000000..31a0795a4e30 --- /dev/null +++ b/packages/image_picker/image_picker_ios/CHANGELOG.md @@ -0,0 +1,13 @@ +## NEXT + +* Removes unnecessary imports. + +## 0.8.5 + +* Switches to an in-package method channel based on Pigeon. +* Fixes invalid casts when selecting multiple images on versions of iOS before + 14.0. + +## 0.8.4+11 + +* Splits from `image_picker` as a federated implementation. diff --git a/packages/image_picker/image_picker_ios/LICENSE b/packages/image_picker/image_picker_ios/LICENSE new file mode 100644 index 000000000000..0be8bbc3e68d --- /dev/null +++ b/packages/image_picker/image_picker_ios/LICENSE @@ -0,0 +1,231 @@ +image_picker + +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- +aFileChooser + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2011 - 2013 Paul Burke + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/image_picker/image_picker_ios/README.md b/packages/image_picker/image_picker_ios/README.md new file mode 100755 index 000000000000..e9fc2cfe61e7 --- /dev/null +++ b/packages/image_picker/image_picker_ios/README.md @@ -0,0 +1,11 @@ +# image\_picker\_ios + +The iOS implementation of [`image_picker`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `image_picker` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/image_picker +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/image_picker/image_picker_ios/example/README.md b/packages/image_picker/image_picker_ios/example/README.md new file mode 100755 index 000000000000..86d5c23ba209 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/README.md @@ -0,0 +1,8 @@ +# image_picker_example + +Demonstrates how to use the `image_picker` plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/image_picker/image_picker_ios/example/integration_test/image_picker_test.dart b/packages/image_picker/image_picker_ios/example/integration_test/image_picker_test.dart new file mode 100644 index 000000000000..2b82b4bda5e4 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/integration_test/image_picker_test.dart @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('placeholder test', (WidgetTester tester) async {}); +} diff --git a/packages/image_picker/image_picker_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/image_picker/image_picker_ios/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100755 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 9.0 + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Flutter/Debug.xcconfig b/packages/image_picker/image_picker_ios/example/ios/Flutter/Debug.xcconfig new file mode 100755 index 000000000000..9803018ca79d --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Generated.xcconfig" +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" diff --git a/packages/image_picker/image_picker_ios/example/ios/Flutter/Release.xcconfig b/packages/image_picker/image_picker_ios/example/ios/Flutter/Release.xcconfig new file mode 100755 index 000000000000..a4a8c604e13d --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Generated.xcconfig" +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" diff --git a/packages/image_picker/image_picker_ios/example/ios/Podfile b/packages/image_picker/image_picker_ios/example/ios/Podfile new file mode 100644 index 000000000000..5bc7b7e85717 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Podfile @@ -0,0 +1,45 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + + target 'RunnerTests' do + platform :ios, '9.0' + inherit! :search_paths + # Pods for testing + pod 'OCMock', '~> 3.8.1' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..2847bfd85046 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,796 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 334733FC266813EE00DCC49E /* ImageUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */; }; + 334733FD266813F100DCC49E /* MetaDataUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 680049252280D736006DD6AB /* MetaDataUtilTests.m */; }; + 334733FE266813F400DCC49E /* PhotoAssetUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */; }; + 334733FF266813FA00DCC49E /* ImagePickerTestImages.m in Sources */ = {isa = PBXBuildFile; fileRef = F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */; }; + 33473400266813FD00DCC49E /* ImagePickerPluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */; }; + 3A72BAD3FAE6E0FA9D80826B /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 35AE65F25E0B8C8214D8372B /* libPods-RunnerTests.a */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 5C9513011EC38BD300040975 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C9513001EC38BD300040975 /* GeneratedPluginRegistrant.m */; }; + 680049382280F2B9006DD6AB /* pngImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 680049352280F2B8006DD6AB /* pngImage.png */; }; + 680049392280F2B9006DD6AB /* jpgImage.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 680049362280F2B8006DD6AB /* jpgImage.jpg */; }; + 6801C8392555D726009DAF8D /* ImagePickerFromGalleryUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6801C8382555D726009DAF8D /* ImagePickerFromGalleryUITests.m */; }; + 86430DF9272D71E9002D9D6C /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; + 86E9A893272754860017E6E0 /* PickerSaveImageToPathOperationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 86E9A892272754860017E6E0 /* PickerSaveImageToPathOperationTests.m */; }; + 86E9A894272754A30017E6E0 /* webpImage.webp in Resources */ = {isa = PBXBuildFile; fileRef = 86E9A88F272747B90017E6E0 /* webpImage.webp */; }; + 86E9A895272769130017E6E0 /* pngImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 680049352280F2B8006DD6AB /* pngImage.png */; }; + 86E9A896272769150017E6E0 /* jpgImage.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 680049362280F2B8006DD6AB /* jpgImage.jpg */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 9FC8F0E9229FA49E00C8D58F /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; + 9FC8F0EC229FA68500C8D58F /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; + BE6173D826A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */; }; + F4F7A436CCA4BF276270A3AE /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EC32F6993F4529982D9519F1 /* libPods-Runner.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 334733F72668136400DCC49E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; + 6801C83B2555D726009DAF8D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0C7B151765FD4249454C49AD /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 334733F22668136400DCC49E /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 334733F62668136400DCC49E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 35AE65F25E0B8C8214D8372B /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5A9D31B91557877A0E8EF3E7 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 5C9512FF1EC38BD300040975 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 5C9513001EC38BD300040975 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 680049252280D736006DD6AB /* MetaDataUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MetaDataUtilTests.m; sourceTree = ""; }; + 680049352280F2B8006DD6AB /* pngImage.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = pngImage.png; sourceTree = ""; }; + 680049362280F2B8006DD6AB /* jpgImage.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = jpgImage.jpg; sourceTree = ""; }; + 6801632E632668F4349764C9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 6801C8362555D726009DAF8D /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 6801C8382555D726009DAF8D /* ImagePickerFromGalleryUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerFromGalleryUITests.m; sourceTree = ""; }; + 6801C83A2555D726009DAF8D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ImagePickerPluginTests.m; sourceTree = ""; }; + 68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PhotoAssetUtilTests.m; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 86E9A88F272747B90017E6E0 /* webpImage.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = webpImage.webp; sourceTree = ""; }; + 86E9A892272754860017E6E0 /* PickerSaveImageToPathOperationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PickerSaveImageToPathOperationTests.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = gifImage.gif; sourceTree = ""; }; + 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImageUtilTests.m; sourceTree = ""; }; + BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerFromLimitedGalleryUITests.m; sourceTree = ""; }; + DC6FCAAD4E7580C9B3C2E21D /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + EC32F6993F4529982D9519F1 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F78AF3172342D9D7008449C7 /* ImagePickerTestImages.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ImagePickerTestImages.h; sourceTree = ""; }; + F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerTestImages.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 334733EF2668136400DCC49E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A72BAD3FAE6E0FA9D80826B /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6801C8332555D726009DAF8D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F4F7A436CCA4BF276270A3AE /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 334733F32668136400DCC49E /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */, + 680049252280D736006DD6AB /* MetaDataUtilTests.m */, + 68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */, + F78AF3172342D9D7008449C7 /* ImagePickerTestImages.h */, + F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */, + 68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */, + 86E9A892272754860017E6E0 /* PickerSaveImageToPathOperationTests.m */, + 334733F62668136400DCC49E /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 680049282280E33D006DD6AB /* TestImages */ = { + isa = PBXGroup; + children = ( + 86E9A88F272747B90017E6E0 /* webpImage.webp */, + 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */, + 680049362280F2B8006DD6AB /* jpgImage.jpg */, + 680049352280F2B8006DD6AB /* pngImage.png */, + ); + path = TestImages; + sourceTree = ""; + }; + 6801C8372555D726009DAF8D /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */, + 6801C8382555D726009DAF8D /* ImagePickerFromGalleryUITests.m */, + 6801C83A2555D726009DAF8D /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; + 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { + isa = PBXGroup; + children = ( + 6801632E632668F4349764C9 /* Pods-Runner.debug.xcconfig */, + 5A9D31B91557877A0E8EF3E7 /* Pods-Runner.release.xcconfig */, + DC6FCAAD4E7580C9B3C2E21D /* Pods-RunnerTests.debug.xcconfig */, + 0C7B151765FD4249454C49AD /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 680049282280E33D006DD6AB /* TestImages */, + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 334733F32668136400DCC49E /* RunnerTests */, + 6801C8372555D726009DAF8D /* RunnerUITests */, + 97C146EF1CF9000F007C117D /* Products */, + 840012C8B5EDBCF56B0E4AC1 /* Pods */, + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 6801C8362555D726009DAF8D /* RunnerUITests.xctest */, + 334733F22668136400DCC49E /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 5C9512FF1EC38BD300040975 /* GeneratedPluginRegistrant.h */, + 5C9513001EC38BD300040975 /* GeneratedPluginRegistrant.m */, + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { + isa = PBXGroup; + children = ( + EC32F6993F4529982D9519F1 /* libPods-Runner.a */, + 35AE65F25E0B8C8214D8372B /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 334733F12668136400DCC49E /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 334733F92668136400DCC49E /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + B8739A4353234497CF76B597 /* [CP] Check Pods Manifest.lock */, + 334733EE2668136400DCC49E /* Sources */, + 334733EF2668136400DCC49E /* Frameworks */, + 334733F02668136400DCC49E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 334733F82668136400DCC49E /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 334733F22668136400DCC49E /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 6801C8352555D726009DAF8D /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6801C83F2555D726009DAF8D /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + 6801C8322555D726009DAF8D /* Sources */, + 6801C8332555D726009DAF8D /* Frameworks */, + 6801C8342555D726009DAF8D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6801C83C2555D726009DAF8D /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = 6801C8362555D726009DAF8D /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Original; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 334733F12668136400DCC49E = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 6801C8352555D726009DAF8D = { + CreatedOnToolsVersion = 11.7; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + SystemCapabilities = { + com.apple.BackgroundModes = { + enabled = 1; + }; + }; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 334733F12668136400DCC49E /* RunnerTests */, + 6801C8352555D726009DAF8D /* RunnerUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 334733F02668136400DCC49E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 86430DF9272D71E9002D9D6C /* gifImage.gif in Resources */, + 86E9A894272754A30017E6E0 /* webpImage.webp in Resources */, + 86E9A895272769130017E6E0 /* pngImage.png in Resources */, + 86E9A896272769150017E6E0 /* jpgImage.jpg in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6801C8342555D726009DAF8D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9FC8F0EC229FA68500C8D58F /* gifImage.gif in Resources */, + 680049382280F2B9006DD6AB /* pngImage.png in Resources */, + 680049392280F2B9006DD6AB /* jpgImage.jpg in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 9FC8F0E9229FA49E00C8D58F /* gifImage.gif in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + B8739A4353234497CF76B597 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 334733EE2668136400DCC49E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 334733FD266813F100DCC49E /* MetaDataUtilTests.m in Sources */, + 334733FF266813FA00DCC49E /* ImagePickerTestImages.m in Sources */, + 86E9A893272754860017E6E0 /* PickerSaveImageToPathOperationTests.m in Sources */, + 334733FC266813EE00DCC49E /* ImageUtilTests.m in Sources */, + 33473400266813FD00DCC49E /* ImagePickerPluginTests.m in Sources */, + 334733FE266813F400DCC49E /* PhotoAssetUtilTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6801C8322555D726009DAF8D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6801C8392555D726009DAF8D /* ImagePickerFromGalleryUITests.m in Sources */, + BE6173D826A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 5C9513011EC38BD300040975 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 334733F82668136400DCC49E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 334733F72668136400DCC49E /* PBXContainerItemProxy */; + }; + 6801C83C2555D726009DAF8D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 6801C83B2555D726009DAF8D /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 334733FA2668136400DCC49E /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DC6FCAAD4E7580C9B3C2E21D /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + 334733FB2668136400DCC49E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0C7B151765FD4249454C49AD /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; + 6801C83D2555D726009DAF8D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + 6801C83E2555D726009DAF8D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; + }; + name = Release; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.imagePickerExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.imagePickerExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 334733F92668136400DCC49E /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 334733FA2668136400DCC49E /* Debug */, + 334733FB2668136400DCC49E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6801C83F2555D726009DAF8D /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6801C83D2555D726009DAF8D /* Debug */, + 6801C83E2555D726009DAF8D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100755 index 000000000000..919434a6254f --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100755 index 000000000000..9b24f28c25cc --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerUITests.xcscheme b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerUITests.xcscheme new file mode 100644 index 000000000000..1a97d9638346 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerUITests.xcscheme @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/image_picker/image_picker_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100755 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/image_picker/image_picker_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/.gitignore b/packages/image_picker/image_picker_ios/example/ios/Runner/.gitignore new file mode 100755 index 000000000000..0cab08d0bdd7 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/.gitignore @@ -0,0 +1,2 @@ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/AppDelegate.h b/packages/image_picker/image_picker_ios/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/AppDelegate.h @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/AppDelegate.m b/packages/image_picker/image_picker_ios/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..b790a0a52635 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/AppDelegate.m @@ -0,0 +1,16 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 000000000000..d225b3c2cfe2 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,121 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100755 index 000000000000..28c6bf03016f Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100755 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100755 index 000000000000..f091b6b0bca8 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100755 index 000000000000..4cde12118dda Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100755 index 000000000000..d0ef06e7edb8 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100755 index 000000000000..dcdc2306c285 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100755 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100755 index 000000000000..c8f9ed8f5cee Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100755 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100755 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100755 index 000000000000..75b2d164a5a9 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100755 index 000000000000..c4df70d39da7 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100755 index 000000000000..6a84f41e14e2 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100755 index 000000000000..d0e1f5853602 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/Contents.json b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/Contents.json new file mode 100644 index 000000000000..da4a164c9186 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/image_picker/image_picker_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100755 index 000000000000..ebf48f603974 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Base.lproj/Main.storyboard b/packages/image_picker/image_picker_ios/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100755 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Info.plist b/packages/image_picker/image_picker_ios/example/ios/Runner/Info.plist new file mode 100755 index 000000000000..f9c1909383ca --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/Info.plist @@ -0,0 +1,59 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + image_picker_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSCameraUsageDescription + Used to demonstrate image picker plugin + NSMicrophoneUsageDescription + Used to capture audio for image picker plugin + NSPhotoLibraryUsageDescription + Used to demonstrate image picker plugin + UIBackgroundModes + + remote-notification + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/main.m b/packages/image_picker/image_picker_ios/example/ios/Runner/main.m new file mode 100644 index 000000000000..f143297b30d6 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/main.m @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "AppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m similarity index 68% rename from packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m rename to packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m index 5f3287400c5e..04d491131d5b 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m @@ -4,8 +4,8 @@ #import "ImagePickerTestImages.h" -@import image_picker; -@import image_picker.Test; +@import image_picker_ios; +@import image_picker_ios.Test; @import XCTest; #import @@ -47,14 +47,15 @@ - (void)testPluginPickImageDeviceBack { // Run test FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickImage" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; [plugin setImagePickerControllerOverrides:@[ controller ]]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + + [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraRear] + maxSize:[[FLTMaxSize alloc] init] + quality:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceRear); } @@ -78,14 +79,15 @@ - (void)testPluginPickImageDeviceFront { // Run test FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickImage" - arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; [plugin setImagePickerControllerOverrides:@[ controller ]]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + + [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraFront] + maxSize:[[FLTMaxSize alloc] init] + quality:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceFront); } @@ -109,14 +111,14 @@ - (void)testPluginPickVideoDeviceBack { // Run test FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickVideo" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; [plugin setImagePickerControllerOverrides:@[ controller ]]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + + [plugin pickVideoWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraRear] + maxDuration:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceRear); } @@ -141,14 +143,14 @@ - (void)testPluginPickVideoDeviceFront { // Run test FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickVideo" - arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; [plugin setImagePickerControllerOverrides:@[ controller ]]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + + [plugin pickVideoWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraFront] + maxDuration:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceFront); } @@ -165,17 +167,12 @@ - (void)testPickMultiImageShouldUseUIImagePickerControllerOnPreiOS14 { FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; [plugin setImagePickerControllerOverrides:@[ mockUIImagePicker ]]; - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"pickMultiImage" - arguments:@{ - @"maxWidth" : @(100), - @"maxHeight" : @(200), - @"imageQuality" : @(50), - }]; - - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + [plugin pickMultiImageWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] + quality:@(50) + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; OCMVerify(times(1), [mockUIImagePicker setSourceType:UIImagePickerControllerSourceTypePhotoLibrary]); } @@ -187,17 +184,15 @@ - (void)testPluginPickImageDeviceCancelClickMultipleTimes { return; } FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickImage" - arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; plugin.imagePickerControllerOverrides = @[ controller ]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; - plugin.result = ^(id result) { - }; + [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraRear] + maxSize:[[FLTMaxSize alloc] init] + quality:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; // To ensure the flow does not crash by multiple cancel call [plugin imagePickerControllerDidCancel:controller]; @@ -208,14 +203,15 @@ - (void)testPluginPickImageDeviceCancelClickMultipleTimes { - (void)testPickingVideoWithDuration { FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = [FlutterMethodCall - methodCallWithMethodName:@"pickVideo" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0), @"maxDuration" : @95}]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; [plugin setImagePickerControllerOverrides:@[ controller ]]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + + [plugin pickVideoWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraRear] + maxDuration:@(95) + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; + XCTAssertEqual(controller.videoMaximumDuration, 95); } @@ -231,37 +227,17 @@ - (void)testViewController { XCTAssertEqual([plugin viewControllerWithWindow:window], vc2); } -- (void)testPluginMultiImagePathIsNil { - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - - dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); - __block FlutterError *pickImageResult = nil; - - plugin.result = ^(id _Nullable r) { - pickImageResult = r; - dispatch_semaphore_signal(resultSemaphore); - }; - [plugin handleSavedPathList:nil]; - - dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); - - XCTAssertEqualObjects(pickImageResult.code, @"create_error"); -} - - (void)testPluginMultiImagePathHasNullItem { FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - NSMutableArray *pathList = [NSMutableArray new]; - - [pathList addObject:[NSNull null]]; dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); __block FlutterError *pickImageResult = nil; - - plugin.result = ^(id _Nullable r) { - pickImageResult = r; - dispatch_semaphore_signal(resultSemaphore); - }; - [plugin handleSavedPathList:pathList]; + plugin.callContext = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^(NSArray *_Nullable result, FlutterError *_Nullable error) { + pickImageResult = error; + dispatch_semaphore_signal(resultSemaphore); + }]; + [plugin sendCallResultWithSavedPathList:@[ [NSNull null] ]]; dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); @@ -270,19 +246,17 @@ - (void)testPluginMultiImagePathHasNullItem { - (void)testPluginMultiImagePathHasItem { FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - NSString *savedPath = @"test"; - NSMutableArray *pathList = [NSMutableArray new]; - - [pathList addObject:savedPath]; + NSArray *pathList = @[ @"test" ]; dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); __block id pickImageResult = nil; - plugin.result = ^(id _Nullable r) { - pickImageResult = r; - dispatch_semaphore_signal(resultSemaphore); - }; - [plugin handleSavedPathList:pathList]; + plugin.callContext = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^(NSArray *_Nullable result, FlutterError *_Nullable error) { + pickImageResult = result; + dispatch_semaphore_signal(resultSemaphore); + }]; + [plugin sendCallResultWithSavedPathList:pathList]; dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerTestImages.h b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerTestImages.h similarity index 100% rename from packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerTestImages.h rename to packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerTestImages.h diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerTestImages.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerTestImages.m similarity index 100% rename from packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerTestImages.m rename to packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerTestImages.m diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/ImageUtilTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImageUtilTests.m similarity index 97% rename from packages/image_picker/image_picker/example/ios/RunnerTests/ImageUtilTests.m rename to packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImageUtilTests.m index 9b9719f88116..e449a84b80bb 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerTests/ImageUtilTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImageUtilTests.m @@ -4,8 +4,8 @@ #import "ImagePickerTestImages.h" -@import image_picker; -@import image_picker.Test; +@import image_picker_ios; +@import image_picker_ios.Test; @import XCTest; @interface ImageUtilTests : XCTestCase diff --git a/packages/image_picker/image_picker/example/ios/RunnerUITests/Info.plist b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/Info.plist similarity index 100% rename from packages/image_picker/image_picker/example/ios/RunnerUITests/Info.plist rename to packages/image_picker/image_picker_ios/example/ios/RunnerTests/Info.plist diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/MetaDataUtilTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/MetaDataUtilTests.m similarity index 98% rename from packages/image_picker/image_picker/example/ios/RunnerTests/MetaDataUtilTests.m rename to packages/image_picker/image_picker_ios/example/ios/RunnerTests/MetaDataUtilTests.m index 4160c51cc600..b684a214570b 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerTests/MetaDataUtilTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/MetaDataUtilTests.m @@ -4,8 +4,8 @@ #import "ImagePickerTestImages.h" -@import image_picker; -@import image_picker.Test; +@import image_picker_ios; +@import image_picker_ios.Test; @import XCTest; @interface MetaDataUtilTests : XCTestCase diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/PhotoAssetUtilTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m similarity index 99% rename from packages/image_picker/image_picker/example/ios/RunnerTests/PhotoAssetUtilTests.m rename to packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m index 97b4b6cd8eb3..d211ea3f91df 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerTests/PhotoAssetUtilTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m @@ -4,8 +4,8 @@ #import "ImagePickerTestImages.h" -@import image_picker; -@import image_picker.Test; +@import image_picker_ios; +@import image_picker_ios.Test; @import XCTest; @interface PhotoAssetUtilTests : XCTestCase diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m similarity index 98% rename from packages/image_picker/image_picker/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m rename to packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m index f94db83d5696..688f5fbee032 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m @@ -5,8 +5,8 @@ #import #import -@import image_picker; -@import image_picker.Test; +@import image_picker_ios; +@import image_picker_ios.Test; @import XCTest; @interface PickerSaveImageToPathOperationTests : XCTestCase diff --git a/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m similarity index 100% rename from packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m rename to packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m diff --git a/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m similarity index 100% rename from packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m rename to packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m diff --git a/packages/local_auth/local_auth/example/ios/RunnerTests/Info.plist b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/Info.plist similarity index 100% rename from packages/local_auth/local_auth/example/ios/RunnerTests/Info.plist rename to packages/image_picker/image_picker_ios/example/ios/RunnerUITests/Info.plist diff --git a/packages/image_picker/image_picker_ios/example/ios/TestImages/gifImage.gif b/packages/image_picker/image_picker_ios/example/ios/TestImages/gifImage.gif new file mode 100644 index 000000000000..5f989fcf40c7 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/TestImages/gifImage.gif differ diff --git a/packages/image_picker/image_picker_ios/example/ios/TestImages/jpgImage.jpg b/packages/image_picker/image_picker_ios/example/ios/TestImages/jpgImage.jpg new file mode 100644 index 000000000000..12b2dc17624c Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/TestImages/jpgImage.jpg differ diff --git a/packages/image_picker/image_picker_ios/example/ios/TestImages/pngImage.png b/packages/image_picker/image_picker_ios/example/ios/TestImages/pngImage.png new file mode 100644 index 000000000000..d7ad7d3968e9 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/TestImages/pngImage.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/TestImages/webpImage.webp b/packages/image_picker/image_picker_ios/example/ios/TestImages/webpImage.webp new file mode 100644 index 000000000000..ab7d40d83968 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/TestImages/webpImage.webp differ diff --git a/packages/image_picker/image_picker_ios/example/ios/image_picker_exampleTests/Info.plist b/packages/image_picker/image_picker_ios/example/ios/image_picker_exampleTests/Info.plist new file mode 100644 index 000000000000..6c40a6cd0c4a --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/image_picker_exampleTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/image_picker/image_picker_ios/example/lib/main.dart b/packages/image_picker/image_picker_ios/example/lib/main.dart new file mode 100755 index 000000000000..48eee35445da --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/lib/main.dart @@ -0,0 +1,467 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:video_player/video_player.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Image Picker Demo', + home: MyHomePage(title: 'Image Picker Example'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key, this.title}) : super(key: key); + + final String? title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + List? _imageFileList; + + set _imageFile(XFile? value) { + _imageFileList = value == null ? null : [value]; + } + + dynamic _pickImageError; + bool isVideo = false; + + VideoPlayerController? _controller; + VideoPlayerController? _toBeDisposed; + String? _retrieveDataError; + + final ImagePickerPlatform _picker = ImagePickerPlatform.instance; + final TextEditingController maxWidthController = TextEditingController(); + final TextEditingController maxHeightController = TextEditingController(); + final TextEditingController qualityController = TextEditingController(); + + Future _playVideo(XFile? file) async { + if (file != null && mounted) { + await _disposeVideoController(); + late VideoPlayerController controller; + if (kIsWeb) { + controller = VideoPlayerController.network(file.path); + } else { + controller = VideoPlayerController.file(File(file.path)); + } + _controller = controller; + // In web, most browsers won't honor a programmatic call to .play + // if the video has a sound track (and is not muted). + // Mute the video so it auto-plays in web! + // This is not needed if the call to .play is the result of user + // interaction (clicking on a "play" button, for example). + const double volume = kIsWeb ? 0.0 : 1.0; + await controller.setVolume(volume); + await controller.initialize(); + await controller.setLooping(true); + await controller.play(); + setState(() {}); + } + } + + Future _onImageButtonPressed(ImageSource source, + {BuildContext? context, bool isMultiImage = false}) async { + if (_controller != null) { + await _controller!.setVolume(0.0); + } + if (isVideo) { + final XFile? file = await _picker.getVideo( + source: source, maxDuration: const Duration(seconds: 10)); + await _playVideo(file); + } else if (isMultiImage) { + await _displayPickImageDialog(context!, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List? pickedFileList = await _picker.getMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _imageFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else { + await _displayPickImageDialog(context!, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _imageFile = pickedFile; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } + } + + @override + void deactivate() { + if (_controller != null) { + _controller!.setVolume(0.0); + _controller!.pause(); + } + super.deactivate(); + } + + @override + void dispose() { + _disposeVideoController(); + maxWidthController.dispose(); + maxHeightController.dispose(); + qualityController.dispose(); + super.dispose(); + } + + Future _disposeVideoController() async { + if (_toBeDisposed != null) { + await _toBeDisposed!.dispose(); + } + _toBeDisposed = _controller; + _controller = null; + } + + Widget _previewVideo() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_controller == null) { + return const Text( + 'You have not yet picked a video', + textAlign: TextAlign.center, + ); + } + return Padding( + padding: const EdgeInsets.all(10.0), + child: AspectRatioVideo(_controller), + ); + } + + Widget _previewImages() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_imageFileList != null) { + return Semantics( + child: ListView.builder( + key: UniqueKey(), + itemBuilder: (BuildContext context, int index) { + // Why network for web? + // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform + return Semantics( + label: 'image_picker_example_picked_image', + child: kIsWeb + ? Image.network(_imageFileList![index].path) + : Image.file(File(_imageFileList![index].path)), + ); + }, + itemCount: _imageFileList!.length, + ), + label: 'image_picker_example_picked_images'); + } else if (_pickImageError != null) { + return Text( + 'Pick image error: $_pickImageError', + textAlign: TextAlign.center, + ); + } else { + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + } + } + + Widget _handlePreview() { + if (isVideo) { + return _previewVideo(); + } else { + return _previewImages(); + } + } + + Future retrieveLostData() async { + final LostDataResponse response = await _picker.getLostData(); + if (response.isEmpty) { + return; + } + if (response.file != null) { + if (response.type == RetrieveType.video) { + isVideo = true; + await _playVideo(response.file); + } else { + isVideo = false; + setState(() { + _imageFile = response.file; + _imageFileList = response.files; + }); + } + } else { + _retrieveDataError = response.exception!.code; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title!), + ), + body: Center( + child: !kIsWeb && defaultTargetPlatform == TargetPlatform.android + ? FutureBuilder( + future: retrieveLostData(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + case ConnectionState.done: + return _handlePreview(); + default: + if (snapshot.hasError) { + return Text( + 'Pick image/video error: ${snapshot.error}}', + textAlign: TextAlign.center, + ); + } else { + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + } + } + }, + ) + : _handlePreview(), + ), + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Semantics( + label: 'image_picker_example_from_gallery', + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed(ImageSource.gallery, context: context); + }, + heroTag: 'image0', + tooltip: 'Pick Image from gallery', + child: const Icon(Icons.photo), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + ); + }, + heroTag: 'image1', + tooltip: 'Pick Multiple Image from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'image2', + tooltip: 'Take a Photo', + child: const Icon(Icons.camera_alt), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + isVideo = true; + _onImageButtonPressed(ImageSource.gallery); + }, + heroTag: 'video0', + tooltip: 'Pick Video from gallery', + child: const Icon(Icons.video_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + isVideo = true; + _onImageButtonPressed(ImageSource.camera); + }, + heroTag: 'video1', + tooltip: 'Take a Video', + child: const Icon(Icons.videocam), + ), + ), + ], + ), + ); + } + + Text? _getRetrieveErrorWidget() { + if (_retrieveDataError != null) { + final Text result = Text(_retrieveDataError!); + _retrieveDataError = null; + return result; + } + return null; + } + + Future _displayPickImageDialog( + BuildContext context, OnPickImageCallback onPick) async { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Add optional parameters'), + content: Column( + children: [ + TextField( + controller: maxWidthController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxWidth if desired'), + ), + TextField( + controller: maxHeightController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxHeight if desired'), + ), + TextField( + controller: qualityController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: 'Enter quality if desired'), + ), + ], + ), + actions: [ + TextButton( + child: const Text('CANCEL'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('PICK'), + onPressed: () { + final double? width = maxWidthController.text.isNotEmpty + ? double.parse(maxWidthController.text) + : null; + final double? height = maxHeightController.text.isNotEmpty + ? double.parse(maxHeightController.text) + : null; + final int? quality = qualityController.text.isNotEmpty + ? int.parse(qualityController.text) + : null; + onPick(width, height, quality); + Navigator.of(context).pop(); + }), + ], + ); + }); + } +} + +typedef OnPickImageCallback = void Function( + double? maxWidth, double? maxHeight, int? quality); + +class AspectRatioVideo extends StatefulWidget { + const AspectRatioVideo(this.controller); + + final VideoPlayerController? controller; + + @override + AspectRatioVideoState createState() => AspectRatioVideoState(); +} + +class AspectRatioVideoState extends State { + VideoPlayerController? get controller => widget.controller; + bool initialized = false; + + void _onVideoControllerUpdate() { + if (!mounted) { + return; + } + if (initialized != controller!.value.isInitialized) { + initialized = controller!.value.isInitialized; + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + controller!.addListener(_onVideoControllerUpdate); + } + + @override + void dispose() { + controller!.removeListener(_onVideoControllerUpdate); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (initialized) { + return Center( + child: AspectRatio( + aspectRatio: controller!.value.aspectRatio, + child: VideoPlayer(controller!), + ), + ); + } else { + return Container(); + } + } +} diff --git a/packages/image_picker/image_picker_ios/example/pubspec.yaml b/packages/image_picker/image_picker_ios/example/pubspec.yaml new file mode 100755 index 000000000000..a47893d7687f --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: image_picker_example +description: Demonstrates how to use the image_picker plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + image_picker_ios: + # When depending on this package from a real application you should use: + # image_picker_ios: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + image_picker_platform_interface: ^2.3.0 + video_player: ^2.1.4 + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/image_picker/image_picker_ios/example/test_driver/integration_test.dart b/packages/image_picker/image_picker_ios/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/image_picker/image_picker/ios/Assets/.gitkeep b/packages/image_picker/image_picker_ios/ios/Assets/.gitkeep similarity index 100% rename from packages/image_picker/image_picker/ios/Assets/.gitkeep rename to packages/image_picker/image_picker_ios/ios/Assets/.gitkeep diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.h similarity index 100% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.h rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.h diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.m similarity index 100% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.m rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.m diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.h similarity index 100% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.h rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.h diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.m similarity index 100% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.m diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h similarity index 100% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.h rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m similarity index 100% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.h similarity index 100% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.h rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.h diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m similarity index 63% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m index cc841d6db447..76ed9623a57c 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m @@ -16,31 +16,24 @@ #import "FLTImagePickerMetaDataUtil.h" #import "FLTImagePickerPhotoAssetUtil.h" #import "FLTPHPickerSaveImageToPathOperation.h" +#import "messages.g.h" -/** - * Returns the value for the given key in 'dict', or nil if the value is - * NSNull. - */ -id GetNullableValueForKey(NSDictionary *dict, NSString *key) { - id value = dict[key]; - return value == [NSNull null] ? nil : value; +@implementation FLTImagePickerMethodCallContext +- (instancetype)initWithResult:(nonnull FlutterResultAdapter)result { + if (self = [super init]) { + _result = [result copy]; + } + return self; } +@end + +#pragma mark - @interface FLTImagePickerPlugin () -/** - * The maximum amount of images that are allowed to be picked. - */ -@property(assign, nonatomic) int maxImagesAllowed; - -/** - * The arguments that are passed in from the Flutter method call. - */ -@property(copy, nonatomic) NSDictionary *arguments; - /** * The PHPickerViewController instance used to pick multiple * images. @@ -58,19 +51,13 @@ @interface FLTImagePickerPlugin () *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/image_picker" - binaryMessenger:[registrar messenger]]; FLTImagePickerPlugin *instance = [FLTImagePickerPlugin new]; - [registrar addMethodCallDelegate:instance channel:channel]; + FLTImagePickerApiSetup(registrar.messenger, instance); } - (UIImagePickerController *)createImagePickerController { @@ -107,130 +94,180 @@ - (UIViewController *)viewControllerWithWindow:(UIWindow *)window { } /** - * Returns the UIImagePickerControllerCameraDevice to use given [arguments]. - * - * If the cameraDevice value that is fetched from arguments is 1 then returns - * UIImagePickerControllerCameraDeviceFront. If the cameraDevice value that is fetched - * from arguments is 0 then returns UIImagePickerControllerCameraDeviceRear. + * Returns the UIImagePickerControllerCameraDevice to use given [source]. * - * @param arguments that should be used to get cameraDevice value. + * @param source The source specification from Dart. */ -- (UIImagePickerControllerCameraDevice)getCameraDeviceFromArguments:(NSDictionary *)arguments { - NSInteger cameraDevice = [arguments[@"cameraDevice"] intValue]; - return (cameraDevice == 1) ? UIImagePickerControllerCameraDeviceFront - : UIImagePickerControllerCameraDeviceRear; +- (UIImagePickerControllerCameraDevice)cameraDeviceForSource:(FLTSourceSpecification *)source { + switch (source.camera) { + case FLTSourceCameraFront: + return UIImagePickerControllerCameraDeviceFront; + case FLTSourceCameraRear: + return UIImagePickerControllerCameraDeviceRear; + } } -- (void)pickImageWithPHPicker:(int)maxImagesAllowed API_AVAILABLE(ios(14)) { +- (void)launchPHPickerWithContext:(nonnull FLTImagePickerMethodCallContext *)context + API_AVAILABLE(ios(14)) { PHPickerConfiguration *config = [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary]; - config.selectionLimit = maxImagesAllowed; // Setting to zero allow us to pick unlimited photos + config.selectionLimit = context.maxImageCount; config.filter = [PHPickerFilter imagesFilter]; _pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config]; _pickerViewController.delegate = self; _pickerViewController.presentationController.delegate = self; - - self.maxImagesAllowed = maxImagesAllowed; + self.callContext = context; [self checkPhotoAuthorizationForAccessLevel]; } -- (void)launchUIImagePickerWithSource:(int)imageSource { +- (void)launchUIImagePickerWithSource:(nonnull FLTSourceSpecification *)source + context:(nonnull FLTImagePickerMethodCallContext *)context { UIImagePickerController *imagePickerController = [self createImagePickerController]; imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; imagePickerController.delegate = self; imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; + self.callContext = context; - self.maxImagesAllowed = 1; - - switch (imageSource) { - case SOURCE_CAMERA: - [self checkCameraAuthorizationWithImagePicker:imagePickerController]; + switch (source.type) { + case FLTSourceTypeCamera: + [self checkCameraAuthorizationWithImagePicker:imagePickerController + camera:[self cameraDeviceForSource:source]]; break; - case SOURCE_GALLERY: + case FLTSourceTypeGallery: [self checkPhotoAuthorizationWithImagePicker:imagePickerController]; break; default: - self.result([FlutterError errorWithCode:@"invalid_source" - message:@"Invalid image source." - details:nil]); + [self sendCallResultWithError:[FlutterError errorWithCode:@"invalid_source" + message:@"Invalid image source." + details:nil]]; break; } } -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if (self.result) { - self.result([FlutterError errorWithCode:@"multiple_request" - message:@"Cancelled by a second request" - details:nil]); - self.result = nil; - } - - self.result = result; - _arguments = call.arguments; - - if ([@"pickImage" isEqualToString:call.method]) { - int imageSource = [call.arguments[@"source"] intValue]; +#pragma mark - FLTImagePickerApi + +- (void)pickImageWithSource:(nonnull FLTSourceSpecification *)source + maxSize:(nonnull FLTMaxSize *)maxSize + quality:(nullable NSNumber *)imageQuality + completion: + (nonnull void (^)(NSString *_Nullable, FlutterError *_Nullable))completion { + [self cancelInProgressCall]; + FLTImagePickerMethodCallContext *context = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^void(NSArray *paths, FlutterError *error) { + if (paths && paths.count != 1) { + completion(nil, [FlutterError errorWithCode:@"invalid_result" + message:@"Incorrect number of return paths provided" + details:nil]); + } + completion(paths.firstObject, error); + }]; + context.maxSize = maxSize; + context.imageQuality = imageQuality; + context.maxImageCount = 1; - if (imageSource == SOURCE_GALLERY) { // Capture is not possible with PHPicker - if (@available(iOS 14, *)) { - // PHPicker is used - [self pickImageWithPHPicker:1]; - } else { - // UIImagePicker is used - [self launchUIImagePickerWithSource:imageSource]; - } - } else { - [self launchUIImagePickerWithSource:imageSource]; - } - } else if ([@"pickMultiImage" isEqualToString:call.method]) { + if (source.type == FLTSourceTypeGallery) { // Capture is not possible with PHPicker if (@available(iOS 14, *)) { - [self pickImageWithPHPicker:0]; + [self launchPHPickerWithContext:context]; } else { - [self launchUIImagePickerWithSource:SOURCE_GALLERY]; - } - } else if ([@"pickVideo" isEqualToString:call.method]) { - UIImagePickerController *imagePickerController = [self createImagePickerController]; - imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; - imagePickerController.delegate = self; - imagePickerController.mediaTypes = @[ - (NSString *)kUTTypeMovie, (NSString *)kUTTypeAVIMovie, (NSString *)kUTTypeVideo, - (NSString *)kUTTypeMPEG4 - ]; - imagePickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; - - int imageSource = [call.arguments[@"source"] intValue]; - if ([call.arguments[@"maxDuration"] isKindOfClass:[NSNumber class]]) { - NSTimeInterval max = [call.arguments[@"maxDuration"] doubleValue]; - imagePickerController.videoMaximumDuration = max; + [self launchUIImagePickerWithSource:source context:context]; } + } else { + [self launchUIImagePickerWithSource:source context:context]; + } +} - switch (imageSource) { - case SOURCE_CAMERA: - [self checkCameraAuthorizationWithImagePicker:imagePickerController]; - break; - case SOURCE_GALLERY: - [self checkPhotoAuthorizationWithImagePicker:imagePickerController]; - break; - default: - result([FlutterError errorWithCode:@"invalid_source" - message:@"Invalid video source." - details:nil]); - break; - } +- (void)pickMultiImageWithMaxSize:(nonnull FLTMaxSize *)maxSize + quality:(nullable NSNumber *)imageQuality + completion:(nonnull void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion { + FLTImagePickerMethodCallContext *context = + [[FLTImagePickerMethodCallContext alloc] initWithResult:completion]; + context.maxSize = maxSize; + context.imageQuality = imageQuality; + + if (@available(iOS 14, *)) { + [self launchPHPickerWithContext:context]; } else { - result(FlutterMethodNotImplemented); + // Camera is ignored for gallery mode, so the value here is arbitrary. + [self launchUIImagePickerWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeGallery + camera:FLTSourceCameraRear] + context:context]; } } -- (void)showCameraWithImagePicker:(UIImagePickerController *)imagePickerController { +- (void)pickVideoWithSource:(nonnull FLTSourceSpecification *)source + maxDuration:(nullable NSNumber *)maxDurationSeconds + completion: + (nonnull void (^)(NSString *_Nullable, FlutterError *_Nullable))completion { + FLTImagePickerMethodCallContext *context = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^void(NSArray *paths, FlutterError *error) { + if (paths && paths.count != 1) { + completion(nil, [FlutterError errorWithCode:@"invalid_result" + message:@"Incorrect number of return paths provided" + details:nil]); + } + completion(paths.firstObject, error); + }]; + context.maxImageCount = 1; + + UIImagePickerController *imagePickerController = [self createImagePickerController]; + imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; + imagePickerController.delegate = self; + imagePickerController.mediaTypes = @[ + (NSString *)kUTTypeMovie, (NSString *)kUTTypeAVIMovie, (NSString *)kUTTypeVideo, + (NSString *)kUTTypeMPEG4 + ]; + imagePickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; + + if (maxDurationSeconds) { + NSTimeInterval max = [maxDurationSeconds doubleValue]; + imagePickerController.videoMaximumDuration = max; + } + + self.callContext = context; + + switch (source.type) { + case FLTSourceTypeCamera: + [self checkCameraAuthorizationWithImagePicker:imagePickerController + camera:[self cameraDeviceForSource:source]]; + break; + case FLTSourceTypeGallery: + [self checkPhotoAuthorizationWithImagePicker:imagePickerController]; + break; + default: + [self sendCallResultWithError:[FlutterError errorWithCode:@"invalid_source" + message:@"Invalid video source." + details:nil]]; + break; + } +} + +#pragma mark - + +/** + * If a call is still in progress, cancels it by returning an error and then clearing state. + * + * TODO(stuartmorgan): Eliminate this, and instead track context per image picker (e.g., using + * associated objects). + */ +- (void)cancelInProgressCall { + if (self.callContext) { + [self sendCallResultWithError:[FlutterError errorWithCode:@"multiple_request" + message:@"Cancelled by a second request" + details:nil]]; + self.callContext = nil; + } +} + +- (void)showCamera:(UIImagePickerControllerCameraDevice)device + withImagePicker:(UIImagePickerController *)imagePickerController { @synchronized(self) { if (imagePickerController.beingPresented) { return; } } - UIImagePickerControllerCameraDevice device = [self getCameraDeviceFromArguments:_arguments]; // Camera is not available on simulators if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] && [UIImagePickerController isCameraDeviceAvailable:device]) { @@ -254,25 +291,24 @@ - (void)showCameraWithImagePicker:(UIImagePickerController *)imagePickerControll [[self viewControllerWithWindow:nil] presentViewController:cameraErrorAlert animated:YES completion:nil]; - self.result(nil); - self.result = nil; - _arguments = nil; + [self sendCallResultWithSavedPathList:nil]; } } -- (void)checkCameraAuthorizationWithImagePicker:(UIImagePickerController *)imagePickerController { +- (void)checkCameraAuthorizationWithImagePicker:(UIImagePickerController *)imagePickerController + camera:(UIImagePickerControllerCameraDevice)device { AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; switch (status) { case AVAuthorizationStatusAuthorized: - [self showCameraWithImagePicker:imagePickerController]; + [self showCamera:device withImagePicker:imagePickerController]; break; case AVAuthorizationStatusNotDetermined: { [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { dispatch_async(dispatch_get_main_queue(), ^{ if (granted) { - [self showCameraWithImagePicker:imagePickerController]; + [self showCamera:device withImagePicker:imagePickerController]; } else { [self errorNoCameraAccess:AVAuthorizationStatusDenied]; } @@ -352,15 +388,17 @@ - (void)checkPhotoAuthorizationForAccessLevel API_AVAILABLE(ios(14)) { - (void)errorNoCameraAccess:(AVAuthorizationStatus)status { switch (status) { case AVAuthorizationStatusRestricted: - self.result([FlutterError errorWithCode:@"camera_access_restricted" - message:@"The user is not allowed to use the camera." - details:nil]); + [self sendCallResultWithError:[FlutterError + errorWithCode:@"camera_access_restricted" + message:@"The user is not allowed to use the camera." + details:nil]]; break; case AVAuthorizationStatusDenied: default: - self.result([FlutterError errorWithCode:@"camera_access_denied" - message:@"The user did not allow camera access." - details:nil]); + [self sendCallResultWithError:[FlutterError + errorWithCode:@"camera_access_denied" + message:@"The user did not allow camera access." + details:nil]]; break; } } @@ -368,15 +406,17 @@ - (void)errorNoCameraAccess:(AVAuthorizationStatus)status { - (void)errorNoPhotoAccess:(PHAuthorizationStatus)status { switch (status) { case PHAuthorizationStatusRestricted: - self.result([FlutterError errorWithCode:@"photo_access_restricted" - message:@"The user is not allowed to use the photo." - details:nil]); + [self sendCallResultWithError:[FlutterError + errorWithCode:@"photo_access_restricted" + message:@"The user is not allowed to use the photo." + details:nil]]; break; case PHAuthorizationStatusDenied: default: - self.result([FlutterError errorWithCode:@"photo_access_denied" - message:@"The user did not allow photo access." - details:nil]); + [self sendCallResultWithError:[FlutterError + errorWithCode:@"photo_access_denied" + message:@"The user did not allow photo access." + details:nil]]; break; } } @@ -406,31 +446,27 @@ - (NSNumber *)getDesiredImageQuality:(NSNumber *)imageQuality { return imageQuality; } +#pragma mark - UIAdaptivePresentationControllerDelegate + - (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController { - if (self.result != nil) { - self.result(nil); - self.result = nil; - self->_arguments = nil; - } + [self sendCallResultWithSavedPathList:nil]; } +#pragma mark - PHPickerViewControllerDelegate + - (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray *)results API_AVAILABLE(ios(14)) { [picker dismissViewControllerAnimated:YES completion:nil]; if (results.count == 0) { - if (self.result != nil) { - self.result(nil); - self.result = nil; - self->_arguments = nil; - } + [self sendCallResultWithSavedPathList:nil]; return; } dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); dispatch_async(backgroundQueue, ^{ - NSNumber *maxWidth = GetNullableValueForKey(self->_arguments, @"maxWidth"); - NSNumber *maxHeight = GetNullableValueForKey(self->_arguments, @"maxHeight"); - NSNumber *imageQuality = GetNullableValueForKey(self->_arguments, @"imageQuality"); + NSNumber *maxWidth = self.callContext.maxSize.width; + NSNumber *maxHeight = self.callContext.maxSize.height; + NSNumber *imageQuality = self.callContext.imageQuality; NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; NSOperationQueue *operationQueue = [NSOperationQueue new]; NSMutableArray *pathList = [self createNSMutableArrayWithSize:results.count]; @@ -449,11 +485,13 @@ - (void)picker:(PHPickerViewController *)picker } [operationQueue waitUntilAllOperationsAreFinished]; dispatch_async(dispatch_get_main_queue(), ^{ - [self handleSavedPathList:pathList]; + [self sendCallResultWithSavedPathList:pathList]; }); }); } +#pragma mark - + /** * Creates an NSMutableArray of a certain size filled with NSNull objects. * @@ -470,6 +508,8 @@ - (NSMutableArray *)createNSMutableArrayWithSize:(NSUInteger)size { return mutableArray; } +#pragma mark - UIImagePickerControllerDelegate + - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { NSURL *videoURL = info[UIImagePickerControllerMediaURL]; @@ -478,7 +518,7 @@ - (void)imagePickerController:(UIImagePickerController *)picker // further didFinishPickingMediaWithInfo invocations. A nil check is necessary // to prevent below code to be unwantly executed multiple times and cause a // crash. - if (!self.result) { + if (!self.callContext) { return; } if (videoURL != nil) { @@ -493,27 +533,25 @@ - (void)imagePickerController:(UIImagePickerController *)picker [[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; + [self sendCallResultWithError:[FlutterError + errorWithCode:@"flutter_image_picker_copy_video_error" + message:@"Could not cache the video file." + details:nil]]; return; } } videoURL = destination; } } - self.result(videoURL.path); - self.result = nil; - _arguments = nil; + [self sendCallResultWithSavedPathList:@[ videoURL.path ]]; } else { UIImage *image = info[UIImagePickerControllerEditedImage]; if (image == nil) { image = info[UIImagePickerControllerOriginalImage]; } - NSNumber *maxWidth = GetNullableValueForKey(_arguments, @"maxWidth"); - NSNumber *maxHeight = GetNullableValueForKey(_arguments, @"maxHeight"); - NSNumber *imageQuality = GetNullableValueForKey(_arguments, @"imageQuality"); + NSNumber *maxWidth = self.callContext.maxSize.width; + NSNumber *maxHeight = self.callContext.maxSize.height; + NSNumber *imageQuality = self.callContext.imageQuality; NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; PHAsset *originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:info]; @@ -547,14 +585,11 @@ - (void)imagePickerController:(UIImagePickerController *)picker - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [picker dismissViewControllerAnimated:YES completion:nil]; - if (!self.result) { - return; - } - self.result(nil); - self.result = nil; - _arguments = nil; + [self sendCallResultWithSavedPathList:nil]; } +#pragma mark - + - (void)saveImageWithOriginalImageData:(NSData *)originalImageData image:(UIImage *)image maxWidth:(NSNumber *)maxWidth @@ -566,7 +601,7 @@ - (void)saveImageWithOriginalImageData:(NSData *)originalImageData maxWidth:maxWidth maxHeight:maxHeight imageQuality:imageQuality]; - [self handleSavedPathList:@[ savedPath ]]; + [self sendCallResultWithSavedPathList:@[ savedPath ]]; } - (void)saveImageWithPickerInfo:(NSDictionary *)info @@ -575,47 +610,36 @@ - (void)saveImageWithPickerInfo:(NSDictionary *)info NSString *savedPath = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:info image:image imageQuality:imageQuality]; - [self handleSavedPathList:@[ savedPath ]]; + [self sendCallResultWithSavedPathList:@[ savedPath ]]; } -/** - * Applies NSMutableArray on the FLutterResult. - * - * NSString must be returned by FlutterResult if the single image - * mode is active. It is checked by maxImagesAllowed and - * returns the first object of the pathlist. - * - * NSMutableArray must be returned by FlutterResult if the multi-image - * mode is active. After the pathlist count is checked then it returns - * the pathlist. - * - * @param pathList that should be applied to FlutterResult. - */ -- (void)handleSavedPathList:(NSArray *)pathList { - if (!self.result) { +- (void)sendCallResultWithSavedPathList:(nullable NSArray *)pathList { + if (!self.callContext) { return; } - if (pathList) { - if (![pathList containsObject:[NSNull null]]) { - if ((self.maxImagesAllowed == 1)) { - self.result(pathList.firstObject); - } else { - self.result(pathList); - } - } else { - self.result([FlutterError errorWithCode:@"create_error" - message:@"pathList's items should not be null" - details:nil]); - } + if ([pathList containsObject:[NSNull null]]) { + self.callContext.result(nil, [FlutterError errorWithCode:@"create_error" + message:@"pathList's items should not be null" + details:nil]); } else { - // This should never happen. - self.result([FlutterError errorWithCode:@"create_error" - message:@"pathList should not be nil" - details:nil]); + self.callContext.result(pathList, nil); + } + self.callContext = nil; +} + +/** + * Sends the given error via `callContext.result` as the result of the original platform channel + * method call, clearing the in-progress call state. + * + * @param error The error to return. + */ +- (void)sendCallResultWithError:(FlutterError *)error { + if (!self.callContext) { + return; } - self.result = nil; - _arguments = nil; + self.callContext.result(nil, error); + self.callContext = nil; } @end diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h new file mode 100644 index 000000000000..2c4167746c8e --- /dev/null +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h @@ -0,0 +1,95 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This header is available in the Test module. Import via "@import image_picker_ios_ios.Test;" + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * The return hander used for all method calls, which internally adapts the provided result list + * to return either a list or a single element depending on the original call. + */ +typedef void (^FlutterResultAdapter)(NSArray *_Nullable, FlutterError *_Nullable); + +/** + * A container class for context to use when handling a method call from the Dart side. + */ +@interface FLTImagePickerMethodCallContext : NSObject + +/** + * Initializes a new context that calls |result| on completion of the operation. + */ +- (instancetype)initWithResult:(nonnull FlutterResultAdapter)result; + +/** The callback to provide results to the Dart caller. */ +@property(nonatomic, copy, nonnull) FlutterResultAdapter result; + +/** + * The maximum size to enforce on the results. + * + * If nil, no resizing is done. + */ +@property(nonatomic, strong, nullable) FLTMaxSize *maxSize; + +/** + * The image quality to resample the results to. + * + * If nil, no resampling is done. + */ +@property(nonatomic, strong, nullable) NSNumber *imageQuality; + +/** Maximum number of images to select. 0 indicates no maximum. */ +@property(nonatomic, assign) int maxImageCount; + +@end + +#pragma mark - + +/** Methods exposed for unit testing. */ +@interface FLTImagePickerPlugin () + +/** + * The context of the Flutter method call that is currently being handled, if any. + */ +@property(strong, nonatomic, nullable) FLTImagePickerMethodCallContext *callContext; + +/** + * Validates the provided paths list, then sends it via `callContext.result` as the result of the + * original platform channel method call, clearing the in-progress call state. + * + * @param pathList The paths to return. nil indicates a cancelled operation. + */ +- (void)sendCallResultWithSavedPathList:(nullable NSArray *)pathList; + +/** + * Tells the delegate that the user cancelled the pick operation. + * + * Your delegate’s implementation of this method should dismiss the picker view + * by calling the dismissModalViewControllerAnimated: method of the parent + * view controller. + * + * Implementation of this method is optional, but expected. + * + * @param picker The controller object managing the image picker interface. + */ +- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker; + +/** + * Sets UIImagePickerController instances that will be used when a new + * controller would normally be created. Each call to + * createImagePickerController will remove the current first element from + * the array. + * + * Should be used for testing purposes only. + */ +- (void)setImagePickerControllerOverrides: + (NSArray *)imagePickerControllers; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.h similarity index 100% rename from packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.h rename to packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.h diff --git a/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m similarity index 100% rename from packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m rename to packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m diff --git a/packages/image_picker/image_picker/ios/Classes/ImagePickerPlugin.modulemap b/packages/image_picker/image_picker_ios/ios/Classes/ImagePickerPlugin.modulemap similarity index 76% rename from packages/image_picker/image_picker/ios/Classes/ImagePickerPlugin.modulemap rename to packages/image_picker/image_picker_ios/ios/Classes/ImagePickerPlugin.modulemap index dc702ea49fb1..0d60b684a256 100644 --- a/packages/image_picker/image_picker/ios/Classes/ImagePickerPlugin.modulemap +++ b/packages/image_picker/image_picker_ios/ios/Classes/ImagePickerPlugin.modulemap @@ -1,6 +1,6 @@ -framework module image_picker { - umbrella header "image_picker-umbrella.h" - +framework module image_picker_ios { + umbrella header "image_picker_ios-umbrella.h" + export * module * { export * } diff --git a/packages/image_picker/image_picker/ios/Classes/image_picker-umbrella.h b/packages/image_picker/image_picker_ios/ios/Classes/image_picker_ios-umbrella.h similarity index 79% rename from packages/image_picker/image_picker/ios/Classes/image_picker-umbrella.h rename to packages/image_picker/image_picker_ios/ios/Classes/image_picker_ios-umbrella.h index 0d89b2e1f636..0e23d6d9d60a 100644 --- a/packages/image_picker/image_picker/ios/Classes/image_picker-umbrella.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/image_picker_ios-umbrella.h @@ -3,4 +3,4 @@ // found in the LICENSE file. #import -#import +#import diff --git a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h new file mode 100644 index 000000000000..310165f72f4f --- /dev/null +++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h @@ -0,0 +1,61 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.0.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import +@protocol FlutterBinaryMessenger; +@protocol FlutterMessageCodec; +@class FlutterError; +@class FlutterStandardTypedData; + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, FLTSourceCamera) { + FLTSourceCameraRear = 0, + FLTSourceCameraFront = 1, +}; + +typedef NS_ENUM(NSUInteger, FLTSourceType) { + FLTSourceTypeCamera = 0, + FLTSourceTypeGallery = 1, +}; + +@class FLTMaxSize; +@class FLTSourceSpecification; + +@interface FLTMaxSize : NSObject ++ (instancetype)makeWithWidth:(nullable NSNumber *)width height:(nullable NSNumber *)height; +@property(nonatomic, strong, nullable) NSNumber *width; +@property(nonatomic, strong, nullable) NSNumber *height; +@end + +@interface FLTSourceSpecification : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithType:(FLTSourceType)type camera:(FLTSourceCamera)camera; +@property(nonatomic, assign) FLTSourceType type; +@property(nonatomic, assign) FLTSourceCamera camera; +@end + +/// The codec used by FLTImagePickerApi. +NSObject *FLTImagePickerApiGetCodec(void); + +@protocol FLTImagePickerApi +- (void)pickImageWithSource:(FLTSourceSpecification *)source + maxSize:(FLTMaxSize *)maxSize + quality:(nullable NSNumber *)imageQuality + completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion; +- (void)pickMultiImageWithMaxSize:(FLTMaxSize *)maxSize + quality:(nullable NSNumber *)imageQuality + completion:(void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion; +- (void)pickVideoWithSource:(FLTSourceSpecification *)source + maxDuration:(nullable NSNumber *)maxDurationSeconds + completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion; +@end + +extern void FLTImagePickerApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +NS_ASSUME_NONNULL_END diff --git a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m new file mode 100644 index 000000000000..6c91c0ab264f --- /dev/null +++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m @@ -0,0 +1,216 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.0.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import "messages.g.h" +#import + +#if !__has_feature(objc_arc) +#error File requires ARC to be enabled. +#endif + +static NSDictionary *wrapResult(id result, FlutterError *error) { + NSDictionary *errorDict = (NSDictionary *)[NSNull null]; + if (error) { + errorDict = @{ + @"code" : (error.code ? error.code : [NSNull null]), + @"message" : (error.message ? error.message : [NSNull null]), + @"details" : (error.details ? error.details : [NSNull null]), + }; + } + return @{ + @"result" : (result ? result : [NSNull null]), + @"error" : errorDict, + }; +} +static id GetNullableObject(NSDictionary *dict, id key) { + id result = dict[key]; + return (result == [NSNull null]) ? nil : result; +} +static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { + id result = array[key]; + return (result == [NSNull null]) ? nil : result; +} + +@interface FLTMaxSize () ++ (FLTMaxSize *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FLTSourceSpecification () ++ (FLTSourceSpecification *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end + +@implementation FLTMaxSize ++ (instancetype)makeWithWidth:(nullable NSNumber *)width height:(nullable NSNumber *)height { + FLTMaxSize *pigeonResult = [[FLTMaxSize alloc] init]; + pigeonResult.width = width; + pigeonResult.height = height; + return pigeonResult; +} ++ (FLTMaxSize *)fromMap:(NSDictionary *)dict { + FLTMaxSize *pigeonResult = [[FLTMaxSize alloc] init]; + pigeonResult.width = GetNullableObject(dict, @"width"); + pigeonResult.height = GetNullableObject(dict, @"height"); + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.width ? self.width : [NSNull null]), @"width", + (self.height ? self.height : [NSNull null]), @"height", nil]; +} +@end + +@implementation FLTSourceSpecification ++ (instancetype)makeWithType:(FLTSourceType)type camera:(FLTSourceCamera)camera { + FLTSourceSpecification *pigeonResult = [[FLTSourceSpecification alloc] init]; + pigeonResult.type = type; + pigeonResult.camera = camera; + return pigeonResult; +} ++ (FLTSourceSpecification *)fromMap:(NSDictionary *)dict { + FLTSourceSpecification *pigeonResult = [[FLTSourceSpecification alloc] init]; + pigeonResult.type = [GetNullableObject(dict, @"type") integerValue]; + pigeonResult.camera = [GetNullableObject(dict, @"camera") integerValue]; + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:@(self.type), @"type", @(self.camera), @"camera", nil]; +} +@end + +@interface FLTImagePickerApiCodecReader : FlutterStandardReader +@end +@implementation FLTImagePickerApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FLTMaxSize fromMap:[self readValue]]; + + case 129: + return [FLTSourceSpecification fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FLTImagePickerApiCodecWriter : FlutterStandardWriter +@end +@implementation FLTImagePickerApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FLTMaxSize class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FLTSourceSpecification class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FLTImagePickerApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FLTImagePickerApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FLTImagePickerApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FLTImagePickerApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FLTImagePickerApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FLTImagePickerApiCodecReaderWriter *readerWriter = + [[FLTImagePickerApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FLTImagePickerApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickImage" + binaryMessenger:binaryMessenger + codec:FLTImagePickerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(pickImageWithSource:maxSize:quality:completion:)], + @"FLTImagePickerApi api (%@) doesn't respond to " + @"@selector(pickImageWithSource:maxSize:quality:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTSourceSpecification *arg_source = GetNullableObjectAtIndex(args, 0); + FLTMaxSize *arg_maxSize = GetNullableObjectAtIndex(args, 1); + NSNumber *arg_imageQuality = GetNullableObjectAtIndex(args, 2); + [api pickImageWithSource:arg_source + maxSize:arg_maxSize + quality:arg_imageQuality + completion:^(NSString *_Nullable output, FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickMultiImage" + binaryMessenger:binaryMessenger + codec:FLTImagePickerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(pickMultiImageWithMaxSize:quality:completion:)], + @"FLTImagePickerApi api (%@) doesn't respond to " + @"@selector(pickMultiImageWithMaxSize:quality:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTMaxSize *arg_maxSize = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_imageQuality = GetNullableObjectAtIndex(args, 1); + [api pickMultiImageWithMaxSize:arg_maxSize + quality:arg_imageQuality + completion:^(NSArray *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickVideo" + binaryMessenger:binaryMessenger + codec:FLTImagePickerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(pickVideoWithSource:maxDuration:completion:)], + @"FLTImagePickerApi api (%@) doesn't respond to " + @"@selector(pickVideoWithSource:maxDuration:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTSourceSpecification *arg_source = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_maxDurationSeconds = GetNullableObjectAtIndex(args, 1); + [api pickVideoWithSource:arg_source + maxDuration:arg_maxDurationSeconds + completion:^(NSString *_Nullable output, FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} diff --git a/packages/image_picker/image_picker/ios/image_picker.podspec b/packages/image_picker/image_picker_ios/ios/image_picker_ios.podspec similarity index 86% rename from packages/image_picker/image_picker/ios/image_picker.podspec rename to packages/image_picker/image_picker_ios/ios/image_picker_ios.podspec index 2a10b1ce01a8..549c5f09e1f8 100644 --- a/packages/image_picker/image_picker/ios/image_picker.podspec +++ b/packages/image_picker/image_picker_ios/ios/image_picker_ios.podspec @@ -2,7 +2,7 @@ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| - s.name = 'image_picker' + s.name = 'image_picker_ios' s.version = '0.0.1' s.summary = 'Flutter plugin that shows an image picker.' s.description = <<-DESC @@ -12,8 +12,8 @@ Downloaded by pub (not CocoaPods). s.homepage = 'https://github.com/flutter/plugins' s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/image_picker' } - s.documentation_url = 'https://pub.dev/packages/image_picker' + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/image_picker_ios' } + s.documentation_url = 'https://pub.dev/packages/image_picker_ios' s.source_files = 'Classes/**/*.{h,m}' s.public_header_files = 'Classes/**/*.h' s.module_map = 'Classes/ImagePickerPlugin.modulemap' diff --git a/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart new file mode 100644 index 000000000000..3d1413cf0cce --- /dev/null +++ b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart @@ -0,0 +1,209 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +import 'src/messages.g.dart'; + +// Converts an [ImageSource] to the corresponding Pigeon API enum value. +SourceType _convertSource(ImageSource source) { + switch (source) { + case ImageSource.camera: + return SourceType.camera; + case ImageSource.gallery: + return SourceType.gallery; + default: + throw UnimplementedError('Unknown source: $source'); + } +} + +// Converts a [CameraDevice] to the corresponding Pigeon API enum value. +SourceCamera _convertCamera(CameraDevice camera) { + switch (camera) { + case CameraDevice.front: + return SourceCamera.front; + case CameraDevice.rear: + return SourceCamera.rear; + default: + throw UnimplementedError('Unknown camera: $camera'); + } +} + +/// An implementation of [ImagePickerPlatform] for iOS. +class ImagePickerIOS extends ImagePickerPlatform { + final ImagePickerApi _hostApi = ImagePickerApi(); + + /// Registers this class as the default platform implementation. + static void registerWith() { + ImagePickerPlatform.instance = ImagePickerIOS(); + } + + @override + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + final String? path = await _pickImageAsPath( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? PickedFile(path) : null; + } + + @override + Future?> pickMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + final List? paths = await _pickMultiImageAsPath( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + ); + if (paths == null) { + return null; + } + + return paths.map((dynamic path) => PickedFile(path as String)).toList(); + } + + Future?> _pickMultiImageAsPath({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + + // TODO(stuartmorgan): Remove the cast once Pigeon supports non-nullable + // generics, https://github.com/flutter/flutter/issues/97848 + return (await _hostApi.pickMultiImage( + MaxSize(width: maxWidth, height: maxHeight), imageQuality)) + ?.cast(); + } + + Future _pickImageAsPath({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) { + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + + return _hostApi.pickImage( + SourceSpecification( + type: _convertSource(source), + camera: _convertCamera(preferredCameraDevice)), + MaxSize(width: maxWidth, height: maxHeight), + imageQuality, + ); + } + + @override + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final String? path = await _pickVideoAsPath( + source: source, + maxDuration: maxDuration, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? PickedFile(path) : null; + } + + Future _pickVideoAsPath({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) { + return _hostApi.pickVideo( + SourceSpecification( + type: _convertSource(source), + camera: _convertCamera(preferredCameraDevice)), + maxDuration?.inSeconds); + } + + @override + Future getImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + final String? path = await _pickImageAsPath( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? XFile(path) : null; + } + + @override + Future?> getMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + final List? paths = await _pickMultiImageAsPath( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + ); + if (paths == null) { + return null; + } + + return paths.map((String path) => XFile(path)).toList(); + } + + @override + Future getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final String? path = await _pickVideoAsPath( + source: source, + maxDuration: maxDuration, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? XFile(path) : null; + } +} diff --git a/packages/image_picker/image_picker_ios/lib/src/messages.g.dart b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart new file mode 100644 index 000000000000..0c5859e80ac9 --- /dev/null +++ b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart @@ -0,0 +1,194 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.0.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name +// @dart = 2.12 +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; + +import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; + +enum SourceCamera { + rear, + front, +} + +enum SourceType { + camera, + gallery, +} + +class MaxSize { + MaxSize({ + this.width, + this.height, + }); + + double? width; + double? height; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['width'] = width; + pigeonMap['height'] = height; + return pigeonMap; + } + + static MaxSize decode(Object message) { + final Map pigeonMap = message as Map; + return MaxSize( + width: pigeonMap['width'] as double?, + height: pigeonMap['height'] as double?, + ); + } +} + +class SourceSpecification { + SourceSpecification({ + required this.type, + this.camera, + }); + + SourceType type; + SourceCamera? camera; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['type'] = type.index; + pigeonMap['camera'] = camera?.index; + return pigeonMap; + } + + static SourceSpecification decode(Object message) { + final Map pigeonMap = message as Map; + return SourceSpecification( + type: SourceType.values[pigeonMap['type']! as int], + camera: pigeonMap['camera'] != null + ? SourceCamera.values[pigeonMap['camera']! as int] + : null, + ); + } +} + +class _ImagePickerApiCodec extends StandardMessageCodec { + const _ImagePickerApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is MaxSize) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return MaxSize.decode(readValue(buffer)!); + + case 129: + return SourceSpecification.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class ImagePickerApi { + /// Constructor for [ImagePickerApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + ImagePickerApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _ImagePickerApiCodec(); + + Future pickImage(SourceSpecification arg_source, MaxSize arg_maxSize, + int? arg_imageQuality) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickImage', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_source, arg_maxSize, arg_imageQuality]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as String?); + } + } + + Future?> pickMultiImage( + MaxSize arg_maxSize, int? arg_imageQuality) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMultiImage', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_maxSize, arg_imageQuality]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as List?)?.cast(); + } + } + + Future pickVideo( + SourceSpecification arg_source, int? arg_maxDurationSeconds) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickVideo', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_source, arg_maxDurationSeconds]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as String?); + } + } +} diff --git a/packages/image_picker/image_picker_ios/pigeons/copyright.txt b/packages/image_picker/image_picker_ios/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/image_picker/image_picker_ios/pigeons/copyright.txt @@ -0,0 +1,3 @@ +Copyright 2013 The Flutter Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. diff --git a/packages/image_picker/image_picker_ios/pigeons/messages.dart b/packages/image_picker/image_picker_ios/pigeons/messages.dart new file mode 100644 index 000000000000..94ac034606e9 --- /dev/null +++ b/packages/image_picker/image_picker_ios/pigeons/messages.dart @@ -0,0 +1,47 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + dartTestOut: 'test/test_api.dart', + objcHeaderOut: 'ios/Classes/messages.g.h', + objcSourceOut: 'ios/Classes/messages.g.m', + objcOptions: ObjcOptions( + prefix: 'FLT', + ), + copyrightHeader: 'pigeons/copyright.txt', +)) +class MaxSize { + MaxSize(this.width, this.height); + double? width; + double? height; +} + +// Corresponds to `CameraDevice` from the platform interface package. +enum SourceCamera { rear, front } + +// Corresponds to `ImageSource` from the platform interface package. +enum SourceType { camera, gallery } + +class SourceSpecification { + SourceSpecification(this.type, this.camera); + SourceType type; + SourceCamera? camera; +} + +@HostApi(dartHostTestHandler: 'TestHostImagePickerApi') +abstract class ImagePickerApi { + @async + @ObjCSelector('pickImageWithSource:maxSize:quality:') + String? pickImage( + SourceSpecification source, MaxSize maxSize, int? imageQuality); + @async + @ObjCSelector('pickMultiImageWithMaxSize:quality:') + List? pickMultiImage(MaxSize maxSize, int? imageQuality); + @async + @ObjCSelector('pickVideoWithSource:maxDuration:') + String? pickVideo(SourceSpecification source, int? maxDurationSeconds); +} diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml new file mode 100755 index 000000000000..a9cd052be56a --- /dev/null +++ b/packages/image_picker/image_picker_ios/pubspec.yaml @@ -0,0 +1,28 @@ +name: image_picker_ios +description: iOS implementation of the video_picker plugin. +repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/image_picker_ios +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 +version: 0.8.5 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: image_picker + platforms: + ios: + dartPluginClass: ImagePickerIOS + pluginClass: FLTImagePickerPlugin + +dependencies: + flutter: + sdk: flutter + image_picker_platform_interface: ^2.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^5.0.0 + pigeon: ^3.0.2 diff --git a/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart new file mode 100644 index 000000000000..09517f1ef96b --- /dev/null +++ b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart @@ -0,0 +1,937 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_ios/image_picker_ios.dart'; +import 'package:image_picker_ios/src/messages.g.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +import 'test_api.dart'; + +@immutable +class _LoggedMethodCall { + const _LoggedMethodCall(this.name, {required this.arguments}); + final String name; + final Map arguments; + + @override + bool operator ==(Object other) { + return other is _LoggedMethodCall && + name == other.name && + mapEquals(arguments, other.arguments); + } + + @override + int get hashCode => Object.hash(name, arguments); + + @override + String toString() { + return 'MethodCall: $name $arguments'; + } +} + +class _ApiLogger implements TestHostImagePickerApi { + // The value to return from future calls. + dynamic returnValue = ''; + final List<_LoggedMethodCall> calls = <_LoggedMethodCall>[]; + + @override + Future pickImage( + SourceSpecification source, MaxSize maxSize, int? imageQuality) async { + // Flatten arguments for easy comparison. + calls.add(_LoggedMethodCall('pickImage', arguments: { + 'source': source.type, + 'cameraDevice': source.camera, + 'maxWidth': maxSize.width, + 'maxHeight': maxSize.height, + 'imageQuality': imageQuality, + })); + return returnValue as String?; + } + + @override + Future?> pickMultiImage( + MaxSize maxSize, int? imageQuality) async { + calls.add(_LoggedMethodCall('pickMultiImage', arguments: { + 'maxWidth': maxSize.width, + 'maxHeight': maxSize.height, + 'imageQuality': imageQuality, + })); + return returnValue as List?; + } + + @override + Future pickVideo( + SourceSpecification source, int? maxDurationSeconds) async { + calls.add(_LoggedMethodCall('pickVideo', arguments: { + 'source': source.type, + 'cameraDevice': source.camera, + 'maxDuration': maxDurationSeconds, + })); + return returnValue as String?; + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final ImagePickerIOS picker = ImagePickerIOS(); + late _ApiLogger log; + + setUp(() { + log = _ApiLogger(); + TestHostImagePickerApi.setup(log); + }); + + test('registration', () async { + ImagePickerIOS.registerWith(); + expect(ImagePickerPlatform.instance, isA()); + }); + + group('#pickImage', () { + test('passes the image source argument correctly', () async { + await picker.pickImage(source: ImageSource.camera); + await picker.pickImage(source: ImageSource.gallery); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.gallery, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.pickImage(source: ImageSource.camera); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxHeight: 10.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.pickImage( + source: ImageSource.camera, + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear + }), + ], + ); + }); + + test('does not accept a invalid imageQuality argument', () { + expect( + () => picker.pickImage(imageQuality: -1, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(imageQuality: 101, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(imageQuality: -1, source: ImageSource.camera), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(imageQuality: 101, source: ImageSource.camera), + throwsArgumentError, + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.pickImage(source: ImageSource.camera, maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(source: ImageSource.camera, maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + log.returnValue = null; + + expect(await picker.pickImage(source: ImageSource.gallery), isNull); + expect(await picker.pickImage(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.pickImage(source: ImageSource.camera); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.pickImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.front, + }), + ], + ); + }); + }); + + group('#pickMultiImage', () { + test('calls the method correctly', () async { + log.returnValue = ['0', '1']; + await picker.pickMultiImage(); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + log.returnValue = ['0', '1']; + await picker.pickMultiImage(); + await picker.pickMultiImage( + maxWidth: 10.0, + ); + await picker.pickMultiImage( + maxHeight: 10.0, + ); + await picker.pickMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.pickMultiImage( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.pickMultiImage( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.pickMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.pickMultiImage(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.pickMultiImage(maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('does not accept a invalid imageQuality argument', () { + expect( + () => picker.pickMultiImage(imageQuality: -1), + throwsArgumentError, + ); + + expect( + () => picker.pickMultiImage(imageQuality: 101), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + log.returnValue = null; + + expect(await picker.pickMultiImage(), isNull); + }); + }); + + group('#pickVideo', () { + test('passes the image source argument correctly', () async { + await picker.pickVideo(source: ImageSource.camera); + await picker.pickVideo(source: ImageSource.gallery); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'cameraDevice': SourceCamera.rear, + 'maxDuration': null, + }), + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.gallery, + 'cameraDevice': SourceCamera.rear, + 'maxDuration': null, + }), + ], + ); + }); + + test('passes the duration argument correctly', () async { + await picker.pickVideo(source: ImageSource.camera); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10), + ); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1), + ); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(hours: 1), + ); + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': null, + 'cameraDevice': SourceCamera.rear, + }), + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': 10, + 'cameraDevice': SourceCamera.rear, + }), + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': 60, + 'cameraDevice': SourceCamera.rear, + }), + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': 3600, + 'cameraDevice': SourceCamera.rear, + }), + ], + ); + }); + + test('handles a null video path response gracefully', () async { + log.returnValue = null; + + expect(await picker.pickVideo(source: ImageSource.gallery), isNull); + expect(await picker.pickVideo(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.pickVideo(source: ImageSource.camera); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'cameraDevice': SourceCamera.rear, + 'maxDuration': null, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.pickVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front, + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': null, + 'cameraDevice': SourceCamera.front, + }), + ], + ); + }); + }); + + group('#getImage', () { + test('passes the image source argument correctly', () async { + await picker.getImage(source: ImageSource.camera); + await picker.getImage(source: ImageSource.gallery); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.gallery, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.getImage(source: ImageSource.camera); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxHeight: 10.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.getImage( + source: ImageSource.camera, + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear + }), + ], + ); + }); + + test('does not accept a invalid imageQuality argument', () { + expect( + () => picker.getImage(imageQuality: -1, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.getImage(imageQuality: 101, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.getImage(imageQuality: -1, source: ImageSource.camera), + throwsArgumentError, + ); + + expect( + () => picker.getImage(imageQuality: 101, source: ImageSource.camera), + throwsArgumentError, + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.getImage(source: ImageSource.camera, maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.getImage(source: ImageSource.camera, maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + log.returnValue = null; + + expect(await picker.getImage(source: ImageSource.gallery), isNull); + expect(await picker.getImage(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.getImage(source: ImageSource.camera); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.front, + }), + ], + ); + }); + }); + + group('#getMultiImage', () { + test('calls the method correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMultiImage(); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMultiImage(); + await picker.getMultiImage( + maxWidth: 10.0, + ); + await picker.getMultiImage( + maxHeight: 10.0, + ); + await picker.getMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.getMultiImage( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.getMultiImage( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.getMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + log.returnValue = ['0', '1']; + expect( + () => picker.getMultiImage(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImage(maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('does not accept a invalid imageQuality argument', () { + log.returnValue = ['0', '1']; + expect( + () => picker.getMultiImage(imageQuality: -1), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImage(imageQuality: 101), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + log.returnValue = null; + + expect(await picker.getMultiImage(), isNull); + expect(await picker.getMultiImage(), isNull); + }); + }); + + group('#getVideo', () { + test('passes the image source argument correctly', () async { + await picker.getVideo(source: ImageSource.camera); + await picker.getVideo(source: ImageSource.gallery); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'cameraDevice': SourceCamera.rear, + 'maxDuration': null, + }), + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.gallery, + 'cameraDevice': SourceCamera.rear, + 'maxDuration': null, + }), + ], + ); + }); + + test('passes the duration argument correctly', () async { + await picker.getVideo(source: ImageSource.camera); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10), + ); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1), + ); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(hours: 1), + ); + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': null, + 'cameraDevice': SourceCamera.rear, + }), + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': 10, + 'cameraDevice': SourceCamera.rear, + }), + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': 60, + 'cameraDevice': SourceCamera.rear, + }), + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': 3600, + 'cameraDevice': SourceCamera.rear, + }), + ], + ); + }); + + test('handles a null video path response gracefully', () async { + log.returnValue = null; + + expect(await picker.getVideo(source: ImageSource.gallery), isNull); + expect(await picker.getVideo(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.getVideo(source: ImageSource.camera); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'cameraDevice': SourceCamera.rear, + 'maxDuration': null, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front, + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': null, + 'cameraDevice': SourceCamera.front, + }), + ], + ); + }); + }); +} diff --git a/packages/image_picker/image_picker_ios/test/test_api.dart b/packages/image_picker/image_picker_ios/test/test_api.dart new file mode 100644 index 000000000000..d22a26b2489b --- /dev/null +++ b/packages/image_picker/image_picker_ios/test/test_api.dart @@ -0,0 +1,126 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.0.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis +// ignore_for_file: avoid_relative_lib_imports +// @dart = 2.12 +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; +import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +// Manually changed due to https://github.com/flutter/flutter/issues/97744 +import 'package:image_picker_ios/src/messages.g.dart'; + +class _TestHostImagePickerApiCodec extends StandardMessageCodec { + const _TestHostImagePickerApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is MaxSize) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return MaxSize.decode(readValue(buffer)!); + + case 129: + return SourceSpecification.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestHostImagePickerApi { + static const MessageCodec codec = _TestHostImagePickerApiCodec(); + + Future pickImage( + SourceSpecification source, MaxSize maxSize, int? imageQuality); + Future?> pickMultiImage(MaxSize maxSize, int? imageQuality); + Future pickVideo( + SourceSpecification source, int? maxDurationSeconds); + static void setup(TestHostImagePickerApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickImage', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImage was null.'); + final List args = (message as List?)!; + final SourceSpecification? arg_source = + (args[0] as SourceSpecification?); + assert(arg_source != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImage was null, expected non-null SourceSpecification.'); + final MaxSize? arg_maxSize = (args[1] as MaxSize?); + assert(arg_maxSize != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImage was null, expected non-null MaxSize.'); + final int? arg_imageQuality = (args[2] as int?); + final String? output = + await api.pickImage(arg_source!, arg_maxSize!, arg_imageQuality); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMultiImage', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMultiImage was null.'); + final List args = (message as List?)!; + final MaxSize? arg_maxSize = (args[0] as MaxSize?); + assert(arg_maxSize != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMultiImage was null, expected non-null MaxSize.'); + final int? arg_imageQuality = (args[1] as int?); + final List? output = + await api.pickMultiImage(arg_maxSize!, arg_imageQuality); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickVideo', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideo was null.'); + final List args = (message as List?)!; + final SourceSpecification? arg_source = + (args[0] as SourceSpecification?); + assert(arg_source != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideo was null, expected non-null SourceSpecification.'); + final int? arg_maxDurationSeconds = (args[1] as int?); + final String? output = + await api.pickVideo(arg_source!, arg_maxDurationSeconds); + return {'result': output}; + }); + } + } + } +} diff --git a/packages/image_picker/image_picker_windows/AUTHORS b/packages/image_picker/image_picker_windows/AUTHORS new file mode 100644 index 000000000000..5db3d584e6bc --- /dev/null +++ b/packages/image_picker/image_picker_windows/AUTHORS @@ -0,0 +1,7 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +Alexandre Zollinger Chohfi \ No newline at end of file diff --git a/packages/image_picker/image_picker_windows/CHANGELOG.md b/packages/image_picker/image_picker_windows/CHANGELOG.md new file mode 100644 index 000000000000..d98656b849c8 --- /dev/null +++ b/packages/image_picker/image_picker_windows/CHANGELOG.md @@ -0,0 +1,7 @@ +## NEXT + +* Removes unnecessary imports. + +## 0.1.0 + +* Initial Windows support. diff --git a/packages/image_picker/image_picker_windows/LICENSE b/packages/image_picker/image_picker_windows/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/image_picker/image_picker_windows/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/image_picker/image_picker_windows/README.md b/packages/image_picker/image_picker_windows/README.md new file mode 100644 index 000000000000..0b256411b2fc --- /dev/null +++ b/packages/image_picker/image_picker_windows/README.md @@ -0,0 +1,16 @@ +# image\_picker\_windows + +A Windows implementation of [`image_picker`][1]. + +### pickImage() +The arguments `source`, `maxWidth`, `maxHeight`, `imageQuality`, and `preferredCameraDevice` are not supported on Windows. + +### pickVideo() +The arguments `source`, `preferredCameraDevice`, and `maxDuration` are not supported on Windows. + +## Usage + +### Import the package + +This package is not yet [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), which means you need to add +not only the `image_picker`, as well as the `image_picker_windows`. \ No newline at end of file diff --git a/packages/image_picker/image_picker_windows/example/README.md b/packages/image_picker/image_picker_windows/example/README.md new file mode 100644 index 000000000000..ae730a5ec846 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/README.md @@ -0,0 +1,8 @@ +# image_picker_windows_example + +Demonstrates how to use the image_picker_windows plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/image_picker/image_picker_windows/example/lib/main.dart b/packages/image_picker/image_picker_windows/example/lib/main.dart new file mode 100644 index 000000000000..577d6dadf2d9 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/lib/main.dart @@ -0,0 +1,413 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:video_player/video_player.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Image Picker Demo', + home: MyHomePage(title: 'Image Picker Example'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key, this.title}) : super(key: key); + + final String? title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + List? _imageFileList; + + // This must be called from within a setState() callback + set _imageFile(PickedFile? value) { + _imageFileList = value == null ? null : [value]; + } + + dynamic _pickImageError; + bool _isVideo = false; + + VideoPlayerController? _controller; + VideoPlayerController? _toBeDisposed; + String? _retrieveDataError; + + final ImagePickerPlatform _picker = ImagePickerPlatform.instance; + final TextEditingController maxWidthController = TextEditingController(); + final TextEditingController maxHeightController = TextEditingController(); + final TextEditingController qualityController = TextEditingController(); + + Future _playVideo(PickedFile? file) async { + if (file != null && mounted) { + await _disposeVideoController(); + final VideoPlayerController controller = + VideoPlayerController.file(File(file.path)); + _controller = controller; + await controller.setVolume(1.0); + await controller.initialize(); + await controller.setLooping(true); + await controller.play(); + setState(() {}); + } + } + + Future _handleMultiImagePicked(BuildContext? context) async { + await _displayPickImageDialog(context!, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List? pickedFileList = await _picker.pickMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _imageFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } + + Future _handleSingleImagePicked( + BuildContext? context, ImageSource source) async { + await _displayPickImageDialog(context!, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final PickedFile? pickedFile = await _picker.pickImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _imageFile = pickedFile; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } + + Future _onImageButtonPressed(ImageSource source, + {BuildContext? context, bool isMultiImage = false}) async { + if (_controller != null) { + await _controller!.setVolume(0.0); + } + if (_isVideo) { + final PickedFile? file = await _picker.pickVideo( + source: source, maxDuration: const Duration(seconds: 10)); + await _playVideo(file); + } else if (isMultiImage) { + await _handleMultiImagePicked(context); + } else { + await _handleSingleImagePicked(context, source); + } + } + + @override + void deactivate() { + if (_controller != null) { + _controller!.setVolume(0.0); + _controller!.pause(); + } + super.deactivate(); + } + + @override + void dispose() { + _disposeVideoController(); + maxWidthController.dispose(); + maxHeightController.dispose(); + qualityController.dispose(); + super.dispose(); + } + + Future _disposeVideoController() async { + if (_toBeDisposed != null) { + await _toBeDisposed!.dispose(); + } + _toBeDisposed = _controller; + _controller = null; + } + + Widget _previewVideo() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_controller == null) { + return const Text( + 'You have not yet picked a video', + textAlign: TextAlign.center, + ); + } + return Padding( + padding: const EdgeInsets.all(10.0), + child: AspectRatioVideo(_controller), + ); + } + + Widget _previewImages() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_imageFileList != null) { + return Semantics( + child: ListView.builder( + key: UniqueKey(), + itemBuilder: (BuildContext context, int index) { + return Semantics( + label: 'image_picker_example_picked_image', + child: Image.file(File(_imageFileList![index].path)), + ); + }, + itemCount: _imageFileList!.length, + ), + label: 'image_picker_example_picked_images'); + } else if (_pickImageError != null) { + return Text( + 'Pick image error: $_pickImageError', + textAlign: TextAlign.center, + ); + } else { + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + } + } + + Widget _handlePreview() { + if (_isVideo) { + return _previewVideo(); + } else { + return _previewImages(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title!), + ), + body: Center( + child: _handlePreview(), + ), + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Semantics( + label: 'image_picker_example_from_gallery', + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed(ImageSource.gallery, context: context); + }, + heroTag: 'image0', + tooltip: 'Pick Image from gallery', + child: const Icon(Icons.photo), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + ); + }, + heroTag: 'image1', + tooltip: 'Pick Multiple Image from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'image2', + tooltip: 'Take a Photo', + child: const Icon(Icons.camera_alt), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.gallery); + }, + heroTag: 'video0', + tooltip: 'Pick Video from gallery', + child: const Icon(Icons.video_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.camera); + }, + heroTag: 'video1', + tooltip: 'Take a Video', + child: const Icon(Icons.videocam), + ), + ), + ], + ), + ); + } + + Text? _getRetrieveErrorWidget() { + if (_retrieveDataError != null) { + final Text result = Text(_retrieveDataError!); + _retrieveDataError = null; + return result; + } + return null; + } + + Future _displayPickImageDialog( + BuildContext context, OnPickImageCallback onPick) async { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Add optional parameters'), + content: Column( + children: [ + TextField( + controller: maxWidthController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxWidth if desired'), + ), + TextField( + controller: maxHeightController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxHeight if desired'), + ), + TextField( + controller: qualityController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: 'Enter quality if desired'), + ), + ], + ), + actions: [ + TextButton( + child: const Text('CANCEL'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('PICK'), + onPressed: () { + final double? width = maxWidthController.text.isNotEmpty + ? double.parse(maxWidthController.text) + : null; + final double? height = maxHeightController.text.isNotEmpty + ? double.parse(maxHeightController.text) + : null; + final int? quality = qualityController.text.isNotEmpty + ? int.parse(qualityController.text) + : null; + onPick(width, height, quality); + Navigator.of(context).pop(); + }), + ], + ); + }); + } +} + +typedef OnPickImageCallback = void Function( + double? maxWidth, double? maxHeight, int? quality); + +class AspectRatioVideo extends StatefulWidget { + const AspectRatioVideo(this.controller); + + final VideoPlayerController? controller; + + @override + AspectRatioVideoState createState() => AspectRatioVideoState(); +} + +class AspectRatioVideoState extends State { + VideoPlayerController? get controller => widget.controller; + bool initialized = false; + + void _onVideoControllerUpdate() { + if (!mounted) { + return; + } + if (initialized != controller!.value.isInitialized) { + initialized = controller!.value.isInitialized; + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + controller!.addListener(_onVideoControllerUpdate); + } + + @override + void dispose() { + controller!.removeListener(_onVideoControllerUpdate); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (initialized) { + return Center( + child: AspectRatio( + aspectRatio: controller!.value.aspectRatio, + child: VideoPlayer(controller!), + ), + ); + } else { + return Container(); + } + } +} diff --git a/packages/image_picker/image_picker_windows/example/pubspec.yaml b/packages/image_picker/image_picker_windows/example/pubspec.yaml new file mode 100644 index 000000000000..68c9395c6097 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/pubspec.yaml @@ -0,0 +1,27 @@ +name: example +description: Example for image_picker_windows implementation. +publish_to: 'none' +version: 1.0.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + image_picker_windows: + # When depending on this package from a real application you should use: + # image_picker_windows: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: .. + video_player: ^2.1.4 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/image_picker/image_picker_windows/example/windows/.gitignore b/packages/image_picker/image_picker_windows/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/image_picker/image_picker_windows/example/windows/CMakeLists.txt b/packages/image_picker/image_picker_windows/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..1633297a0c7c --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +set(BINARY_NAME "example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/image_picker/image_picker_windows/example/windows/flutter/CMakeLists.txt b/packages/image_picker/image_picker_windows/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..b2e4bd8d658b --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,103 @@ +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/image_picker/image_picker_windows/example/windows/flutter/generated_plugins.cmake b/packages/image_picker/image_picker_windows/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..63eda9b7b59f --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,16 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/CMakeLists.txt b/packages/image_picker/image_picker_windows/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..de2d8916b72b --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/Runner.rc b/packages/image_picker/image_picker_windows/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..5fdea291cf19 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/flutter_window.cpp b/packages/image_picker/image_picker_windows/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..8254bd9ff3c1 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/flutter_window.cpp @@ -0,0 +1,65 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/flutter_window.h b/packages/image_picker/image_picker_windows/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..f1fc669093d0 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/flutter_window.h @@ -0,0 +1,37 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/main.cpp b/packages/image_picker/image_picker_windows/example/windows/runner/main.cpp new file mode 100644 index 000000000000..df379fa0be93 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/main.cpp @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t* command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/resource.h b/packages/image_picker/image_picker_windows/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/resources/app_icon.ico b/packages/image_picker/image_picker_windows/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/image_picker/image_picker_windows/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/runner.exe.manifest b/packages/image_picker/image_picker_windows/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..c977c4a42589 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/utils.cpp b/packages/image_picker/image_picker_windows/example/windows/runner/utils.cpp new file mode 100644 index 000000000000..fb7e945a63b7 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/utils.cpp @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE* unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = + ::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, + nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/utils.h b/packages/image_picker/image_picker_windows/example/windows/runner/utils.h new file mode 100644 index 000000000000..bd81e1e02338 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/utils.h @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/win32_window.cpp b/packages/image_picker/image_picker_windows/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..85aa3614e8ad --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/win32_window.cpp @@ -0,0 +1,241 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { ++g_active_window_count; } + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { return window_handle_; } + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/win32_window.h b/packages/image_picker/image_picker_windows/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..d2a730052223 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/win32_window.h @@ -0,0 +1,99 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart b/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart new file mode 100644 index 000000000000..9bd26c471b4e --- /dev/null +++ b/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart @@ -0,0 +1,167 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_windows/file_selector_windows.dart'; +import 'package:flutter/foundation.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +/// The Windows implementation of [ImagePickerPlatform]. +/// +/// This class implements the `package:image_picker` functionality for +/// Windows. +class ImagePickerWindows extends ImagePickerPlatform { + /// Constructs a ImagePickerWindows. + ImagePickerWindows(); + + /// List of image extensions used when picking images + @visibleForTesting + static const List imageFormats = [ + 'jpg', + 'jpeg', + 'png', + 'bmp', + 'webp', + 'gif', + 'tif', + 'tiff', + 'apng' + ]; + + /// List of video extensions used when picking videos + @visibleForTesting + static const List videoFormats = [ + 'mov', + 'wmv', + 'mkv', + 'mp4', + 'webm', + 'avi', + 'mpeg', + 'mpg' + ]; + + /// The file selector used to prompt the user to select images or videos. + @visibleForTesting + static late FileSelectorPlatform fileSelector = FileSelectorWindows(); + + /// Registers this class as the default instance of [ImagePickerPlatform]. + static void registerWith() { + ImagePickerPlatform.instance = ImagePickerWindows(); + } + + // `maxWidth`, `maxHeight`, `imageQuality` and `preferredCameraDevice` + // arguments are not supported on Windows. If any of these arguments + // is supplied, it'll be silently ignored by the Windows version of + // the plugin. `source` is not implemented for `ImageSource.camera` + // and will throw an exception. + @override + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + final XFile? file = await getImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice); + if (file != null) { + return PickedFile(file.path); + } + return null; + } + + // `preferredCameraDevice` and `maxDuration` arguments are not + // supported on Windows. If any of these arguments is supplied, + // it'll be silently ignored by the Windows version of the plugin. + // `source` is not implemented for `ImageSource.camera` and will + // throw an exception. + @override + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final XFile? file = await getVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration); + if (file != null) { + return PickedFile(file.path); + } + return null; + } + + // `maxWidth`, `maxHeight`, `imageQuality`, and `preferredCameraDevice` + // arguments are not supported on Windows. If any of these arguments + // is supplied, it'll be silently ignored by the Windows version + // of the plugin. `source` is not implemented for `ImageSource.camera` + // and will throw an exception. + @override + Future getImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + if (source != ImageSource.gallery) { + // TODO(azchohfi): Support ImageSource.camera. + // See https://github.com/flutter/flutter/issues/102115 + throw UnimplementedError( + 'ImageSource.gallery is currently the only supported source on Windows'); + } + final XTypeGroup typeGroup = + XTypeGroup(label: 'images', extensions: imageFormats); + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + return file; + } + + // `preferredCameraDevice` and `maxDuration` arguments are not + // supported on Windows. If any of these arguments is supplied, + // it'll be silently ignored by the Windows version of the plugin. + // `source` is not implemented for `ImageSource.camera` and will + // throw an exception. + @override + Future getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + if (source != ImageSource.gallery) { + // TODO(azchohfi): Support ImageSource.camera. + // See https://github.com/flutter/flutter/issues/102115 + throw UnimplementedError( + 'ImageSource.gallery is currently the only supported source on Windows'); + } + final XTypeGroup typeGroup = + XTypeGroup(label: 'videos', extensions: videoFormats); + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + return file; + } + + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not + // supported on Windows. If any of these arguments is supplied, + // it'll be silently ignored by the Windows version of the plugin. + @override + Future> getMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + final XTypeGroup typeGroup = + XTypeGroup(label: 'images', extensions: imageFormats); + final List files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + return files; + } +} diff --git a/packages/image_picker/image_picker_windows/pubspec.yaml b/packages/image_picker/image_picker_windows/pubspec.yaml new file mode 100644 index 000000000000..eec41f7bfa0d --- /dev/null +++ b/packages/image_picker/image_picker_windows/pubspec.yaml @@ -0,0 +1,29 @@ +name: image_picker_windows +description: Windows platform implementation of image_picker +repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/image_picker_windows +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 +version: 0.1.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + implements: image_picker + platforms: + windows: + dartPluginClass: ImagePickerWindows + +dependencies: + file_selector_platform_interface: ^2.0.4 + file_selector_windows: ^0.8.2 + flutter: + sdk: flutter + image_picker_platform_interface: ^2.4.3 + +dev_dependencies: + build_runner: ^2.1.5 + flutter_test: + sdk: flutter + mockito: ^5.0.16 diff --git a/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart new file mode 100644 index 000000000000..c3df2d80679f --- /dev/null +++ b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart @@ -0,0 +1,128 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:image_picker_windows/image_picker_windows.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'image_picker_windows_test.mocks.dart'; + +@GenerateMocks([FileSelectorPlatform]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$ImagePickerWindows()', () { + final ImagePickerWindows plugin = ImagePickerWindows(); + late MockFileSelectorPlatform mockFileSelectorPlatform; + + setUp(() { + mockFileSelectorPlatform = MockFileSelectorPlatform(); + + when(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .thenAnswer((_) async => null); + + when(mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .thenAnswer((_) async => List.empty()); + + ImagePickerWindows.fileSelector = mockFileSelectorPlatform; + }); + + test('registered instance', () { + ImagePickerWindows.registerWith(); + expect(ImagePickerPlatform.instance, isA()); + }); + + group('images', () { + test('pickImage passes the accepted type groups correctly', () async { + await plugin.pickImage(source: ImageSource.gallery); + + expect( + verify(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))) + .captured + .single[0] + .extensions, + ImagePickerWindows.imageFormats); + }); + + test('pickImage throws UnimplementedError when source is camera', + () async { + expect(() async => await plugin.pickImage(source: ImageSource.camera), + throwsA(isA())); + }); + + test('getImage passes the accepted type groups correctly', () async { + await plugin.getImage(source: ImageSource.gallery); + + expect( + verify(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))) + .captured + .single[0] + .extensions, + ImagePickerWindows.imageFormats); + }); + + test('getImage throws UnimplementedError when source is camera', + () async { + expect(() async => await plugin.getImage(source: ImageSource.camera), + throwsA(isA())); + }); + + test('getMultiImage passes the accepted type groups correctly', () async { + await plugin.getMultiImage(); + + expect( + verify(mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))) + .captured + .single[0] + .extensions, + ImagePickerWindows.imageFormats); + }); + }); + group('videos', () { + test('pickVideo passes the accepted type groups correctly', () async { + await plugin.pickVideo(source: ImageSource.gallery); + + expect( + verify(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))) + .captured + .single[0] + .extensions, + ImagePickerWindows.videoFormats); + }); + + test('pickVideo throws UnimplementedError when source is camera', + () async { + expect(() async => await plugin.pickVideo(source: ImageSource.camera), + throwsA(isA())); + }); + + test('getVideo passes the accepted type groups correctly', () async { + await plugin.getVideo(source: ImageSource.gallery); + + expect( + verify(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))) + .captured + .single[0] + .extensions, + ImagePickerWindows.videoFormats); + }); + + test('getVideo throws UnimplementedError when source is camera', + () async { + expect(() async => await plugin.getVideo(source: ImageSource.camera), + throwsA(isA())); + }); + }); + }); +} diff --git a/packages/image_picker/image_picker_windows/test/image_picker_windows_test.mocks.dart b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.mocks.dart new file mode 100644 index 000000000000..be2dd2ac5768 --- /dev/null +++ b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.mocks.dart @@ -0,0 +1,78 @@ +// Mocks generated by Mockito 5.1.0 from annotations +// in image_picker_windows/example/windows/flutter/ephemeral/.plugin_symlinks/image_picker_windows/test/image_picker_windows_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i3; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart' + as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +/// A class which mocks [FileSelectorPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFileSelectorPlatform extends _i1.Mock + implements _i2.FileSelectorPlatform { + MockFileSelectorPlatform() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i2.XFile?> openFile( + {List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText}) => + (super.noSuchMethod( + Invocation.method(#openFile, [], { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText + }), + returnValue: Future<_i2.XFile?>.value()) as _i3.Future<_i2.XFile?>); + @override + _i3.Future> openFiles( + {List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText}) => + (super.noSuchMethod( + Invocation.method(#openFiles, [], { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText + }), + returnValue: Future>.value(<_i2.XFile>[])) + as _i3.Future>); + @override + _i3.Future getSavePath( + {List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText}) => + (super.noSuchMethod( + Invocation.method(#getSavePath, [], { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #suggestedName: suggestedName, + #confirmButtonText: confirmButtonText + }), + returnValue: Future.value()) as _i3.Future); + @override + _i3.Future getDirectoryPath( + {String? initialDirectory, String? confirmButtonText}) => + (super.noSuchMethod( + Invocation.method(#getDirectoryPath, [], { + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText + }), + returnValue: Future.value()) as _i3.Future); +} diff --git a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md index 6253a23f48db..24ef9eaffd1d 100644 --- a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Removes unnecessary imports. +* Adds OS version support information to README. + ## 3.0.2 * Adds additional explanation on why it is important to complete a purchase. diff --git a/packages/in_app_purchase/in_app_purchase/README.md b/packages/in_app_purchase/in_app_purchase/README.md index dbc8e8a17bc4..258eba7f50f4 100644 --- a/packages/in_app_purchase/in_app_purchase/README.md +++ b/packages/in_app_purchase/in_app_purchase/README.md @@ -5,6 +5,10 @@ A storefront-independent API for purchases in Flutter apps. This plugin supports in-app purchases (_IAP_) through an _underlying store_, which can be the App Store (on iOS) or Google Play (on Android). +| | Android | iOS | +|-------------|---------|------| +| **Support** | SDK 16+ | 9.0+ | +

An animated image of the iOS in-app purchase UI @@ -32,7 +36,7 @@ your app with each store. Both stores have extensive guides: * [App Store documentation](https://developer.apple.com/in-app-purchase/) * [Google Play documentation](https://developer.android.com/google/play/billing/billing_overview) -> NOTE: Further in this document the App Store and Google Play will be referred +> NOTE: Further in this document the App Store and Google Play will be referred > to as "the store" or "the underlying store", except when a feature is specific > to a particular store. @@ -195,12 +199,12 @@ if (_isConsumable(productDetails)) { ### Completing a purchase The `InAppPurchase.purchaseStream` will send purchase updates after initiating -the purchase flow using `InAppPurchase.buyConsumable` or -`InAppPurchase.buyNonConsumable`. After verifying the purchase receipt and the -delivering the content to the user it is important to call +the purchase flow using `InAppPurchase.buyConsumable` or +`InAppPurchase.buyNonConsumable`. After verifying the purchase receipt and the +delivering the content to the user it is important to call `InAppPurchase.completePurchase` to tell the underlying store that the -purchase has been completed. Calling `InAppPurchase.completePurchase` will -inform the underlying store that the app verified and processed the +purchase has been completed. Calling `InAppPurchase.completePurchase` will +inform the underlying store that the app verified and processed the purchase and the store can proceed to finalize the transaction and bill the end user's payment account. diff --git a/packages/in_app_purchase/in_app_purchase/lib/in_app_purchase.dart b/packages/in_app_purchase/in_app_purchase/lib/in_app_purchase.dart index df05d8ce86c3..d550e48ebc3a 100644 --- a/packages/in_app_purchase/in_app_purchase/lib/in_app_purchase.dart +++ b/packages/in_app_purchase/in_app_purchase/lib/in_app_purchase.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:in_app_purchase_android/in_app_purchase_android.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md index a871cfafb4a2..2657d504ac91 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Removes unnecessary imports. + +## 0.2.2+3 + +* Migrates from `ui.hash*` to `Object.hash*`. +* Updates minimum Flutter version to 2.5.0. + ## 0.2.2+2 * Internal code cleanup for stricter analysis options. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart index 416eb5680770..7378aeb84cfc 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart @@ -10,8 +10,6 @@ import 'package:json_annotation/json_annotation.dart'; import '../../billing_client_wrappers.dart'; import '../channel.dart'; -import 'purchase_wrapper.dart'; -import 'sku_details_wrapper.dart'; part 'billing_client_wrapper.g.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart index 653e5147f9b0..efaf07984df7 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter/foundation.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -71,7 +69,7 @@ class PurchaseWrapper { } @override - int get hashCode => hashValues( + int get hashCode => Object.hash( orderId, packageName, purchaseTime, @@ -238,7 +236,7 @@ class PurchaseHistoryRecordWrapper { } @override - int get hashCode => hashValues(purchaseTime, purchaseToken, signature, sku, + int get hashCode => Object.hash(purchaseTime, purchaseToken, signature, sku, originalJson, developerPayload); } @@ -278,7 +276,7 @@ class PurchasesResultWrapper { } @override - int get hashCode => hashValues(billingResult, responseCode, purchasesList); + int get hashCode => Object.hash(billingResult, responseCode, purchasesList); /// The detailed description of the status of the operation. final BillingResultWrapper billingResult; @@ -326,7 +324,7 @@ class PurchasesHistoryResult { } @override - int get hashCode => hashValues(billingResult, purchaseHistoryRecordList); + int get hashCode => Object.hash(billingResult, purchaseHistoryRecordList); /// The detailed description of the status of the [BillingClient.queryPurchaseHistory]. final BillingResultWrapper billingResult; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart index 53595c572901..07f9d8f29abf 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -161,7 +159,7 @@ class SkuDetailsWrapper { @override int get hashCode { - return hashValues( + return Object.hash( description.hashCode, freeTrialPeriod.hashCode, introductoryPrice.hashCode, @@ -216,7 +214,7 @@ class SkuDetailsResponseWrapper { } @override - int get hashCode => hashValues(billingResult, skuDetailsList); + int get hashCode => Object.hash(billingResult, skuDetailsList); } /// Params containing the response code and the debug message from the Play Billing API response. @@ -261,5 +259,5 @@ class BillingResultWrapper { } @override - int get hashCode => hashValues(responseCode, debugMessage); + int get hashCode => Object.hash(responseCode, debugMessage); } diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart index 61af75688a01..14dd69364497 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart @@ -7,7 +7,6 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:in_app_purchase_android/in_app_purchase_android.dart'; -import 'package:in_app_purchase_android/src/in_app_purchase_android_platform_addition.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import '../billing_client_wrappers.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart index 9bcfc3d1b007..dd629164866f 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart @@ -7,7 +7,6 @@ import 'package:in_app_purchase_android/in_app_purchase_android.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import '../billing_client_wrappers.dart'; -import 'types/types.dart'; /// Contains InApp Purchase features that are only available on PlayStore. class InAppPurchaseAndroidPlatformAddition diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml index 0a667c672945..62888e6dfb73 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,11 +2,11 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/plugins/tree/main/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.2.2+2 +version: 0.2.2+3 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.5.0" flutter: plugin: diff --git a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart index 9d2045b4c229..c87d0e39f0c2 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart @@ -8,7 +8,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_android/billing_client_wrappers.dart'; import 'package:in_app_purchase_android/in_app_purchase_android.dart'; import 'package:in_app_purchase_android/src/channel.dart'; -import 'package:in_app_purchase_android/src/in_app_purchase_android_platform_addition.dart'; import 'billing_client_wrappers/purchase_wrapper_test.dart'; import 'stub_in_app_purchase_platform.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md index a52d8d244f5f..f7d3268d1cae 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Removes unnecessary imports. + ## 1.3.1 * Update to use the `verify` method introduced in plugin_platform_interface 2.1.0. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/types/product_details_test.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/types/product_details_test.dart index 737f0d00b392..486f38fa850c 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/types/product_details_test.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/types/product_details_test.dart @@ -4,7 +4,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; -import 'package:in_app_purchase_platform_interface/src/types/purchase_status.dart'; void main() { group('Constructor Tests', () { diff --git a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md index 087c6e3095db..403ee32be2ae 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md @@ -1,3 +1,15 @@ +## NEXT + +* Removes unnecessary imports. + +## 0.3.0+5 + +* Migrates from `ui.hash*` to `Object.hash*`. + +## 0.3.0+4 + +* Ensures that `NSError` instances with an unexpected value for the `userInfo` field don't crash the app, but send an explanatory message instead. + ## 0.3.0+3 * Implements transaction caching for StoreKit ensuring transactions are delivered to the Flutter client. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m index 0f689f602de8..c4e1ac1d059d 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m @@ -158,6 +158,56 @@ - (void)testError { XCTAssertEqualObjects(map, self.errorMap); } +- (void)testErrorWithNSNumberAsUserInfo { + NSError *error = [NSError errorWithDomain:SKErrorDomain code:3 userInfo:@{@"key" : @42}]; + NSDictionary *expectedMap = + @{@"domain" : SKErrorDomain, @"code" : @3, @"userInfo" : @{@"key" : @42}}; + NSDictionary *map = [FIAObjectTranslator getMapFromNSError:error]; + XCTAssertEqualObjects(expectedMap, map); +} + +- (void)testErrorWithMultipleUnderlyingErrors { + NSError *underlyingErrorOne = [NSError errorWithDomain:SKErrorDomain code:2 userInfo:nil]; + NSError *underlyingErrorTwo = [NSError errorWithDomain:SKErrorDomain code:1 userInfo:nil]; + NSError *mainError = [NSError + errorWithDomain:SKErrorDomain + code:3 + userInfo:@{@"underlyingErrors" : @[ underlyingErrorOne, underlyingErrorTwo ]}]; + NSDictionary *expectedMap = @{ + @"domain" : SKErrorDomain, + @"code" : @3, + @"userInfo" : @{ + @"underlyingErrors" : @[ + @{@"domain" : SKErrorDomain, @"code" : @2, @"userInfo" : @{}}, + @{@"domain" : SKErrorDomain, @"code" : @1, @"userInfo" : @{}} + ] + } + }; + NSDictionary *map = [FIAObjectTranslator getMapFromNSError:mainError]; + XCTAssertEqualObjects(expectedMap, map); +} + +- (void)testErrorWithUnsupportedUserInfo { + NSError *error = [NSError errorWithDomain:SKErrorDomain + code:3 + userInfo:@{@"user_info" : [[NSObject alloc] init]}]; + NSDictionary *expectedMap = @{ + @"domain" : SKErrorDomain, + @"code" : @3, + @"userInfo" : @{ + @"user_info" : [NSString + stringWithFormat: + @"Unable to encode native userInfo object of type %@ to map. Please submit an " + @"issue at https://github.com/flutter/flutter/issues/new with the title " + @"\"[in_app_purchase_storekit] Unable to encode userInfo of type %@\" and add " + @"reproduction steps and the error details in the description field.", + [NSObject class], [NSObject class]] + } + }; + NSDictionary *map = [FIAObjectTranslator getMapFromNSError:error]; + XCTAssertEqualObjects(expectedMap, map); +} + - (void)testLocaleToMap { if (@available(iOS 10.0, *)) { NSLocale *system = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m index 3ceb512abb10..5d87a68de67c 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m @@ -167,20 +167,43 @@ + (NSDictionary *)getMapFromNSError:(NSError *)error { if (!error) { return nil; } + NSMutableDictionary *userInfo = [NSMutableDictionary new]; for (NSErrorUserInfoKey key in error.userInfo) { id value = error.userInfo[key]; - if ([value isKindOfClass:[NSError class]]) { - userInfo[key] = [FIAObjectTranslator getMapFromNSError:value]; - } else if ([value isKindOfClass:[NSURL class]]) { - userInfo[key] = [value absoluteString]; - } else { - userInfo[key] = value; - } + userInfo[key] = [FIAObjectTranslator encodeNSErrorUserInfo:value]; } return @{@"code" : @(error.code), @"domain" : error.domain ?: @"", @"userInfo" : userInfo}; } ++ (id)encodeNSErrorUserInfo:(id)value { + if ([value isKindOfClass:[NSError class]]) { + return [FIAObjectTranslator getMapFromNSError:value]; + } else if ([value isKindOfClass:[NSURL class]]) { + return [value absoluteString]; + } else if ([value isKindOfClass:[NSNumber class]]) { + return value; + } else if ([value isKindOfClass:[NSString class]]) { + return value; + } else if ([value isKindOfClass:[NSArray class]]) { + NSMutableArray *errors = [[NSMutableArray alloc] init]; + for (id error in value) { + [errors addObject:[FIAObjectTranslator encodeNSErrorUserInfo:error]]; + } + return errors; + } else { + return [NSString + stringWithFormat: + @"Unable to encode native userInfo object of type %@ to map. Please submit an issue at " + @"https://github.com/flutter/flutter/issues/new with the title " + @"\"[in_app_purchase_storekit] " + @"Unable to encode userInfo of type %@\" and add reproduction steps and the error " + @"details in " + @"the description field.", + [value class], [value class]]; + } +} + + (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront { if (!storefront) { return nil; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart index 6db2e59e1485..629355d12d4c 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart @@ -7,7 +7,6 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; -import 'package:in_app_purchase_storekit/src/in_app_purchase_storekit_platform_addition.dart'; import '../in_app_purchase_storekit.dart'; import '../store_kit_wrappers.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 022848281327..70db7da2e275 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:ui' show hashValues; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -13,9 +12,6 @@ import 'package:json_annotation/json_annotation.dart'; import '../channel.dart'; import '../in_app_purchase_storekit_platform.dart'; -import 'sk_payment_queue_delegate_wrapper.dart'; -import 'sk_payment_transaction_wrappers.dart'; -import 'sk_product_wrapper.dart'; part 'sk_payment_queue_wrapper.g.dart'; @@ -365,7 +361,7 @@ class SKError { } @override - int get hashCode => hashValues( + int get hashCode => Object.hash( code, domain, userInfo, @@ -482,7 +478,7 @@ class SKPaymentWrapper { } @override - int get hashCode => hashValues(productIdentifier, applicationUsername, + int get hashCode => Object.hash(productIdentifier, applicationUsername, quantity, simulatesAskToBuyInSandbox, requestData); @override @@ -585,5 +581,5 @@ class SKPaymentDiscountWrapper { @override int get hashCode => - hashValues(identifier, keyIdentifier, nonce, signature, timestamp); + Object.hash(identifier, keyIdentifier, nonce, signature, timestamp); } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart index 4c4c91257d9d..3894721a1f80 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -190,8 +188,8 @@ class SKPaymentTransactionWrapper { } @override - int get hashCode => hashValues(payment, transactionState, originalTransaction, - transactionTimeStamp, transactionIdentifier, error); + int get hashCode => Object.hash(payment, transactionState, + originalTransaction, transactionTimeStamp, transactionIdentifier, error); @override String toString() => _$SKPaymentTransactionWrapperToJson(this).toString(); diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.dart index 105d999d8a69..2354563261fc 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.dart @@ -2,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:ui' show hashValues; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -64,7 +63,7 @@ class SkProductResponseWrapper { } @override - int get hashCode => hashValues(products, invalidProductIdentifiers); + int get hashCode => Object.hash(products, invalidProductIdentifiers); } /// Dart wrapper around StoreKit's [SKProductPeriodUnit](https://developer.apple.com/documentation/storekit/skproductperiodunit?language=objc). @@ -142,7 +141,7 @@ class SKProductSubscriptionPeriodWrapper { } @override - int get hashCode => hashValues(numberOfUnits, unit); + int get hashCode => Object.hash(numberOfUnits, unit); } /// Dart wrapper around StoreKit's [SKProductDiscountPaymentMode](https://developer.apple.com/documentation/storekit/skproductdiscountpaymentmode?language=objc). @@ -232,7 +231,7 @@ class SKProductDiscountWrapper { } @override - int get hashCode => hashValues( + int get hashCode => Object.hash( price, priceLocale, numberOfPeriods, paymentMode, subscriptionPeriod); } @@ -342,7 +341,7 @@ class SKProductWrapper { } @override - int get hashCode => hashValues( + int get hashCode => Object.hash( productIdentifier, localizedTitle, localizedDescription, @@ -410,5 +409,5 @@ class SKPriceLocaleWrapper { } @override - int get hashCode => hashValues(currencySymbol, currencyCode); + int get hashCode => Object.hash(currencySymbol, currencyCode); } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart index 0bf1103a5abd..ff9e9b7db746 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -56,7 +54,7 @@ class SKStorefrontWrapper { } @override - int get hashCode => hashValues( + int get hashCode => Object.hash( countryCode, identifier, ); diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml index 51b5ce7f1614..9693c186119c 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_storekit description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework. repository: https://github.com/flutter/plugins/tree/main/packages/in_app_purchase/in_app_purchase_storekit issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.3.0+3 +version: 0.3.0+5 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart index 9667d789f1f7..7c543750c25e 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart @@ -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:io'; import 'package:flutter/services.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart index fdf80d68a3ea..41329335dcf4 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:in_app_purchase_storekit/src/store_kit_wrappers/sk_product_wrapper.dart'; import 'package:in_app_purchase_storekit/src/types/app_store_product_details.dart'; import 'package:in_app_purchase_storekit/src/types/app_store_purchase_details.dart'; import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; diff --git a/packages/ios_platform_images/CHANGELOG.md b/packages/ios_platform_images/CHANGELOG.md index 416f93661c8d..0a3755afa7e7 100644 --- a/packages/ios_platform_images/CHANGELOG.md +++ b/packages/ios_platform_images/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Removes unnecessary imports. + +## 0.2.0+5 + +* Migrates from `ui.hash*` to `Object.hash*`. +* Adds OS version support information to README. + ## 0.2.0+4 * Internal code cleanup for stricter analysis options. diff --git a/packages/ios_platform_images/README.md b/packages/ios_platform_images/README.md index ada89fcdffec..08dfc3e40b31 100644 --- a/packages/ios_platform_images/README.md +++ b/packages/ios_platform_images/README.md @@ -8,6 +8,10 @@ Flutter images. When loading images from Image.xcassets the device specific variant is chosen ([iOS documentation](https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/image-size-and-resolution/)). +| | iOS | +|-------------|------| +| **Support** | 9.0+ | + ## Usage ### iOS->Flutter Example diff --git a/packages/ios_platform_images/lib/ios_platform_images.dart b/packages/ios_platform_images/lib/ios_platform_images.dart index 23a437d775ef..4064fb312506 100644 --- a/packages/ios_platform_images/lib/ios_platform_images.dart +++ b/packages/ios_platform_images/lib/ios_platform_images.dart @@ -8,7 +8,6 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart' show SynchronousFuture, describeIdentity, immutable, objectRuntimeType; -import 'package:flutter/painting.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; @@ -95,7 +94,7 @@ class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> { /// See [ImageProvider.hashCode]. @override - int get hashCode => hashValues(_futureBytes.hashCode, _futureScale); + int get hashCode => Object.hash(_futureBytes.hashCode, _futureScale); /// See [ImageProvider.toString]. @override diff --git a/packages/ios_platform_images/pubspec.yaml b/packages/ios_platform_images/pubspec.yaml index fe7952abbc3b..7f80714e4c1c 100644 --- a/packages/ios_platform_images/pubspec.yaml +++ b/packages/ios_platform_images/pubspec.yaml @@ -2,7 +2,7 @@ name: ios_platform_images description: A plugin to share images between Flutter and iOS in add-to-app setups. repository: https://github.com/flutter/plugins/tree/main/packages/ios_platform_images issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+ios_platform_images%22 -version: 0.2.0+4 +version: 0.2.0+5 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/local_auth/local_auth/AUTHORS b/packages/local_auth/local_auth/AUTHORS index 493a0b4ef9c2..d5694690c247 100644 --- a/packages/local_auth/local_auth/AUTHORS +++ b/packages/local_auth/local_auth/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Bodhi Mulders diff --git a/packages/local_auth/local_auth/CHANGELOG.md b/packages/local_auth/local_auth/CHANGELOG.md index 9387540d6795..c495acae5a81 100644 --- a/packages/local_auth/local_auth/CHANGELOG.md +++ b/packages/local_auth/local_auth/CHANGELOG.md @@ -1,3 +1,47 @@ +## NEXT + +* Removes unnecessary imports. + +## 2.0.0 + +* Migrates plugin to federated architecture. +* Adds OS version support information to README. +* BREAKING CHANGE: Deprecated method `authenticateWithBiometrics` has been removed. + Use `authenticate` instead. +* BREAKING CHANGE: Enum `BiometricType` has been expanded with options for `strong` and `weak`, + and applications should be updated to handle these accordingly. +* BREAKING CHANGE: Parameters of `authenticate` have been changed. + + Example: + ```dart + // Old way of calling `authenticate`. + Future authenticate( + localizedReason: 'localized reason', + useErrorDialogs: true, + stickyAuth: false, + androidAuthStrings: const AndroidAuthMessages(), + iOSAuthStrings: const IOSAuthMessages(), + sensitiveTransaction: true, + biometricOnly: false, + ); + // New way of calling `authenticate`. + Future authenticate( + localizedReason: 'localized reason', + authMessages: const [ + IOSAuthMessages(), + AndroidAuthMessages() + ], + options: const AuthenticationOptions( + useErrorDialogs: true, + stickyAuth: false, + sensitiveTransaction: true, + biometricOnly: false, + ), + ); + ``` + + + ## 1.1.11 * Adds support `localizedFallbackTitle` in authenticateWithBiometrics on iOS. diff --git a/packages/local_auth/local_auth/README.md b/packages/local_auth/local_auth/README.md index 84470c646e6b..3a60e45a57eb 100644 --- a/packages/local_auth/local_auth/README.md +++ b/packages/local_auth/local_auth/README.md @@ -6,7 +6,11 @@ the user. This means referring to biometric authentication on iOS (Touch ID or lock code) and the fingerprint APIs on Android (introduced in Android 6.0). -## Usage in Dart +| | Android | iOS | +|-------------|-----------|------| +| **Support** | SDK 16+\* | 9.0+ | + +## Usage Import the relevant file: @@ -134,6 +138,11 @@ try { } ``` +### Android + +\* The plugin will build and run on SDK 16+, but `isDeviceSupported()` will +always return false before SDK 23 (Android 6.0). + ## iOS Integration Note that this plugin works with both Touch ID and Face ID. However, to use the latter, diff --git a/packages/local_auth/local_auth/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java b/packages/local_auth/local_auth/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java deleted file mode 100644 index 41868e603ad8..000000000000 --- a/packages/local_auth/local_auth/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.localauth; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.app.Activity; -import android.content.Context; -import androidx.lifecycle.Lifecycle; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.embedding.engine.dart.DartExecutor; -import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import org.junit.Test; - -public class LocalAuthTest { - @Test - public void isDeviceSupportedReturnsFalse() { - final LocalAuthPlugin plugin = new LocalAuthPlugin(); - final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - plugin.onMethodCall(new MethodCall("isDeviceSupported", null), mockResult); - verify(mockResult).success(false); - } - - @Test - public void onDetachedFromActivity_ShouldReleaseActivity() { - final Activity mockActivity = mock(Activity.class); - final ActivityPluginBinding mockActivityBinding = mock(ActivityPluginBinding.class); - when(mockActivityBinding.getActivity()).thenReturn(mockActivity); - - Context mockContext = mock(Context.class); - when(mockActivity.getBaseContext()).thenReturn(mockContext); - - final HiddenLifecycleReference mockLifecycleReference = mock(HiddenLifecycleReference.class); - when(mockActivityBinding.getLifecycle()).thenReturn(mockLifecycleReference); - - final Lifecycle mockLifecycle = mock(Lifecycle.class); - when(mockLifecycleReference.getLifecycle()).thenReturn(mockLifecycle); - - final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class); - final FlutterEngine mockFlutterEngine = mock(FlutterEngine.class); - when(mockPluginBinding.getFlutterEngine()).thenReturn(mockFlutterEngine); - - DartExecutor mockDartExecutor = mock(DartExecutor.class); - when(mockFlutterEngine.getDartExecutor()).thenReturn(mockDartExecutor); - - final LocalAuthPlugin plugin = new LocalAuthPlugin(); - plugin.onAttachedToEngine(mockPluginBinding); - plugin.onAttachedToActivity(mockActivityBinding); - assertNotNull(plugin.getActivity()); - - plugin.onDetachedFromActivity(); - assertNull(plugin.getActivity()); - } -} diff --git a/packages/local_auth/local_auth/example/ios/Podfile b/packages/local_auth/local_auth/example/ios/Podfile index ef20d8e3c010..f7d6a5e68c3a 100644 --- a/packages/local_auth/local_auth/example/ios/Podfile +++ b/packages/local_auth/local_auth/example/ios/Podfile @@ -29,12 +29,6 @@ flutter_ios_podfile_setup target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - - target 'RunnerTests' do - inherit! :search_paths - - pod 'OCMock', '3.5' - end end post_install do |installer| diff --git a/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/project.pbxproj b/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/project.pbxproj index 3de4b94f9d5c..b40fbca4cf66 100644 --- a/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,25 +9,14 @@ /* Begin PBXBuildFile section */ 0CCCD07A2CE24E13C9C1EEA4 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9D274A3F79473B1549B2BBD5 /* libPods-Runner.a */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3398D2E426164AD8005A052F /* FLTLocalAuthPluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3398D2E326164AD8005A052F /* FLTLocalAuthPluginTests.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - B726772E092FC537C9618264 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 719FE2C7EAF8D9A045E09C29 /* libPods-RunnerTests.a */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - 3398D2D226163948005A052F /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { @@ -45,11 +34,8 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3398D2CD26163948005A052F /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 3398D2D126163948005A052F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3398D2DC261649CD005A052F /* liblocal_auth.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = liblocal_auth.a; sourceTree = BUILT_PRODUCTS_DIR; }; 3398D2DF26164A03005A052F /* liblocal_auth.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = liblocal_auth.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 3398D2E326164AD8005A052F /* FLTLocalAuthPluginTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTLocalAuthPluginTests.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 658CDD04B21E4EA92F8EF229 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 719FE2C7EAF8D9A045E09C29 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -71,14 +57,6 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 3398D2CA26163948005A052F /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - B726772E092FC537C9618264 /* libPods-RunnerTests.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -90,15 +68,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 33BF11D226680B2E002967F3 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 3398D2E326164AD8005A052F /* FLTLocalAuthPluginTests.m */, - 3398D2D126163948005A052F /* Info.plist */, - ); - path = RunnerTests; - sourceTree = ""; - }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -113,7 +82,6 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( - 33BF11D226680B2E002967F3 /* RunnerTests */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, @@ -126,7 +94,6 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - 3398D2CD26163948005A052F /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -180,25 +147,6 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 3398D2CC26163948005A052F /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 3398D2D426163948005A052F /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - B5AF6C7A6759E6F38749E537 /* [CP] Check Pods Manifest.lock */, - 3398D2C926163948005A052F /* Sources */, - 3398D2CA26163948005A052F /* Frameworks */, - 3398D2CB26163948005A052F /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 3398D2D326163948005A052F /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 3398D2CD26163948005A052F /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; @@ -226,14 +174,9 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { - 3398D2CC26163948005A052F = { - CreatedOnToolsVersion = 12.4; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; - }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; }; @@ -253,19 +196,11 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - 3398D2CC26163948005A052F /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 3398D2CB26163948005A052F /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -326,39 +261,9 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - B5AF6C7A6759E6F38749E537 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 3398D2C926163948005A052F /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 3398D2E426164AD8005A052F /* FLTLocalAuthPluginTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -371,14 +276,6 @@ }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - 3398D2D326163948005A052F /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 3398D2D226163948005A052F /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -399,53 +296,6 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 3398D2D526163948005A052F /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 99302E79EC77497F2F274D12 /* Pods-RunnerTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = RunnerTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Debug; - }; - 3398D2D626163948005A052F /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = FEA527BB0A821430FEAA1566 /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = RunnerTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Release; - }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -597,15 +447,6 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 3398D2D426163948005A052F /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 3398D2D526163948005A052F /* Debug */, - 3398D2D626163948005A052F /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 58a5d07a15c8..b2af55dd6d37 100644 --- a/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ { _authorized = 'Authenticating'; }); authenticated = await auth.authenticate( - localizedReason: 'Let OS determine authentication method', + localizedReason: 'Let OS determine authentication method', + options: const AuthenticationOptions( useErrorDialogs: true, - stickyAuth: true); + stickyAuth: true, + ), + ); setState(() { _isAuthenticating = false; }); @@ -109,11 +112,14 @@ class _MyAppState extends State { _authorized = 'Authenticating'; }); authenticated = await auth.authenticate( - localizedReason: - 'Scan your fingerprint (or face or whatever) to authenticate', + localizedReason: + 'Scan your fingerprint (or face or whatever) to authenticate', + options: const AuthenticationOptions( useErrorDialogs: true, stickyAuth: true, - biometricOnly: true); + biometricOnly: true, + ), + ); setState(() { _isAuthenticating = false; _authorized = 'Authenticating'; diff --git a/packages/local_auth/local_auth/lib/auth_strings.dart b/packages/local_auth/local_auth/lib/auth_strings.dart deleted file mode 100644 index 3e34659b8dad..000000000000 --- a/packages/local_auth/local_auth/lib/auth_strings.dart +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// This is a temporary ignore to allow us to land a new set of linter rules in a -// series of manageable patches instead of one gigantic PR. It disables some of -// the new lints that are already failing on this plugin, for this plugin. It -// should be deleted and the failing lints addressed as soon as possible. -// ignore_for_file: public_member_api_docs - -import 'package:intl/intl.dart'; - -/// Android side authentication messages. -/// -/// Provides default values for all messages. -class AndroidAuthMessages { - const AndroidAuthMessages({ - this.biometricHint, - this.biometricNotRecognized, - this.biometricRequiredTitle, - this.biometricSuccess, - this.cancelButton, - this.deviceCredentialsRequiredTitle, - this.deviceCredentialsSetupDescription, - this.goToSettingsButton, - this.goToSettingsDescription, - this.signInTitle, - }); - - final String? biometricHint; - final String? biometricNotRecognized; - final String? biometricRequiredTitle; - final String? biometricSuccess; - final String? cancelButton; - final String? deviceCredentialsRequiredTitle; - final String? deviceCredentialsSetupDescription; - final String? goToSettingsButton; - final String? goToSettingsDescription; - final String? signInTitle; - - Map get args { - return { - 'biometricHint': biometricHint ?? androidBiometricHint, - 'biometricNotRecognized': - biometricNotRecognized ?? androidBiometricNotRecognized, - 'biometricSuccess': biometricSuccess ?? androidBiometricSuccess, - 'biometricRequired': - biometricRequiredTitle ?? androidBiometricRequiredTitle, - 'cancelButton': cancelButton ?? androidCancelButton, - 'deviceCredentialsRequired': deviceCredentialsRequiredTitle ?? - androidDeviceCredentialsRequiredTitle, - 'deviceCredentialsSetupDescription': deviceCredentialsSetupDescription ?? - androidDeviceCredentialsSetupDescription, - 'goToSetting': goToSettingsButton ?? goToSettings, - 'goToSettingDescription': - goToSettingsDescription ?? androidGoToSettingsDescription, - 'signInTitle': signInTitle ?? androidSignInTitle, - }; - } -} - -/// iOS side authentication messages. -/// -/// Provides default values for all messages. -class IOSAuthMessages { - const IOSAuthMessages({ - this.lockOut, - this.goToSettingsButton, - this.goToSettingsDescription, - this.cancelButton, - this.localizedFallbackTitle, - }); - - final String? lockOut; - final String? goToSettingsButton; - final String? goToSettingsDescription; - final String? cancelButton; - final String? localizedFallbackTitle; - - Map get args { - return { - 'lockOut': lockOut ?? iOSLockOut, - 'goToSetting': goToSettingsButton ?? goToSettings, - 'goToSettingDescriptionIOS': - goToSettingsDescription ?? iOSGoToSettingsDescription, - 'okButton': cancelButton ?? iOSOkButton, - if (localizedFallbackTitle != null) - 'localizedFallbackTitle': localizedFallbackTitle!, - }; - } -} - -// Strings for local_authentication plugin. Currently supports English. -// Intl.message must be string literals. -String get androidBiometricHint => Intl.message('Verify identity', - desc: - 'Hint message advising the user how to authenticate with biometrics. It is ' - 'used on Android side. Maximum 60 characters.'); - -String get androidBiometricNotRecognized => - Intl.message('Not recognized. Try again.', - desc: 'Message to let the user know that authentication was failed. It ' - 'is used on Android side. Maximum 60 characters.'); - -String get androidBiometricSuccess => Intl.message('Success', - desc: 'Message to let the user know that authentication was successful. It ' - 'is used on Android side. Maximum 60 characters.'); - -String get androidCancelButton => Intl.message('Cancel', - desc: 'Message showed on a button that the user can click to leave the ' - 'current dialog. It is used on Android side. Maximum 30 characters.'); - -String get androidSignInTitle => Intl.message('Authentication required', - desc: 'Message showed as a title in a dialog which indicates the user ' - 'that they need to scan biometric to continue. It is used on ' - 'Android side. Maximum 60 characters.'); - -String get androidBiometricRequiredTitle => Intl.message('Biometric required', - desc: 'Message showed as a title in a dialog which indicates the user ' - 'has not set up biometric authentication on their device. It is used on Android' - ' side. Maximum 60 characters.'); - -String get androidDeviceCredentialsRequiredTitle => Intl.message( - 'Device credentials required', - desc: 'Message showed as a title in a dialog which indicates the user ' - 'has not set up credentials authentication on their device. It is used on Android' - ' side. Maximum 60 characters.'); - -String get androidDeviceCredentialsSetupDescription => Intl.message( - 'Device credentials required', - desc: 'Message advising the user to go to the settings and configure ' - 'device credentials on their device. It shows in a dialog on Android side.'); - -String get goToSettings => Intl.message('Go to settings', - desc: 'Message showed on a button that the user can click to go to ' - 'settings pages from the current dialog. It is used on both Android ' - 'and iOS side. Maximum 30 characters.'); - -String get androidGoToSettingsDescription => Intl.message( - 'Biometric authentication is not set up on your device. Go to ' - '\'Settings > Security\' to add biometric authentication.', - desc: 'Message advising the user to go to the settings and configure ' - 'biometric on their device. It shows in a dialog on Android side.'); - -String get iOSLockOut => Intl.message( - 'Biometric authentication is disabled. Please lock and unlock your screen to ' - 'enable it.', - desc: - 'Message advising the user to re-enable biometrics on their device. It ' - 'shows in a dialog on iOS side.'); - -String get iOSGoToSettingsDescription => Intl.message( - 'Biometric authentication is not set up on your device. Please either enable ' - 'Touch ID or Face ID on your phone.', - desc: - 'Message advising the user to go to the settings and configure Biometrics ' - 'for their device. It shows in a dialog on iOS side.'); - -String get iOSOkButton => Intl.message('OK', - desc: 'Message showed on a button that the user can click to leave the ' - 'current dialog. It is used on iOS side. Maximum 30 characters.'); diff --git a/packages/local_auth/local_auth/lib/local_auth.dart b/packages/local_auth/local_auth/lib/local_auth.dart index 3e925c00e5ae..7c42fedc7755 100644 --- a/packages/local_auth/local_auth/lib/local_auth.dart +++ b/packages/local_auth/local_auth/lib/local_auth.dart @@ -2,181 +2,8 @@ // 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/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; -import 'package:platform/platform.dart'; - -import 'auth_strings.dart'; -import 'error_codes.dart'; - -enum BiometricType { face, fingerprint, iris } - -const MethodChannel _channel = MethodChannel('plugins.flutter.io/local_auth'); - -Platform _platform = const LocalPlatform(); - -@visibleForTesting -void setMockPathProviderPlatform(Platform platform) { - _platform = platform; -} - -/// A Flutter plugin for authenticating the user identity locally. -class LocalAuthentication { - /// The `authenticateWithBiometrics` method has been deprecated. - /// Use `authenticate` with `biometricOnly: true` instead - @Deprecated('Use `authenticate` with `biometricOnly: true` instead') - Future authenticateWithBiometrics({ - required String localizedReason, - bool useErrorDialogs = true, - bool stickyAuth = false, - AndroidAuthMessages androidAuthStrings = const AndroidAuthMessages(), - IOSAuthMessages iOSAuthStrings = const IOSAuthMessages(), - bool sensitiveTransaction = true, - }) => - authenticate( - localizedReason: localizedReason, - useErrorDialogs: useErrorDialogs, - stickyAuth: stickyAuth, - androidAuthStrings: androidAuthStrings, - iOSAuthStrings: iOSAuthStrings, - sensitiveTransaction: sensitiveTransaction, - biometricOnly: true, - ); - - /// Authenticates the user with biometrics available on the device while also - /// allowing the user to use device authentication - pin, pattern, passcode. - /// - /// Returns a [Future] holding true, if the user successfully authenticated, - /// false otherwise. - /// - /// [localizedReason] is the message to show to user while prompting them - /// for authentication. This is typically along the lines of: 'Please scan - /// your finger to access MyApp.'. This must not be empty. - /// - /// [useErrorDialogs] = true means the system will attempt to handle user - /// fixable issues encountered while authenticating. For instance, if - /// fingerprint reader exists on the phone but there's no fingerprint - /// registered, the plugin will attempt to take the user to settings to add - /// one. Anything that is not user fixable, such as no biometric sensor on - /// device, will be returned as a [PlatformException]. - /// - /// [stickyAuth] is used when the application goes into background for any - /// reason while the authentication is in progress. Due to security reasons, - /// the authentication has to be stopped at that time. If stickyAuth is set - /// to true, authentication resumes when the app is resumed. If it is set to - /// false (default), then as soon as app is paused a failure message is sent - /// back to Dart and it is up to the client app to restart authentication or - /// do something else. - /// - /// Construct [AndroidAuthStrings] and [IOSAuthStrings] if you want to - /// customize messages in the dialogs. - /// - /// Setting [sensitiveTransaction] to true enables platform specific - /// precautions. For instance, on face unlock, Android opens a confirmation - /// dialog after the face is recognized to make sure the user meant to unlock - /// their phone. - /// - /// Setting [biometricOnly] to true prevents authenticates from using non-biometric - /// local authentication such as pin, passcode, and passcode. - /// - /// Throws an [PlatformException] if there were technical problems with local - /// authentication (e.g. lack of relevant hardware). This might throw - /// [PlatformException] with error code [otherOperatingSystem] on the iOS - /// simulator. - Future authenticate({ - required String localizedReason, - bool useErrorDialogs = true, - bool stickyAuth = false, - AndroidAuthMessages androidAuthStrings = const AndroidAuthMessages(), - IOSAuthMessages iOSAuthStrings = const IOSAuthMessages(), - bool sensitiveTransaction = true, - bool biometricOnly = false, - }) async { - assert(localizedReason.isNotEmpty); - - final Map args = { - 'localizedReason': localizedReason, - 'useErrorDialogs': useErrorDialogs, - 'stickyAuth': stickyAuth, - 'sensitiveTransaction': sensitiveTransaction, - 'biometricOnly': biometricOnly, - }; - if (_platform.isIOS) { - args.addAll(iOSAuthStrings.args); - } else if (_platform.isAndroid) { - args.addAll(androidAuthStrings.args); - } else { - throw PlatformException( - code: otherOperatingSystem, - message: 'Local authentication does not support non-Android/iOS ' - 'operating systems.', - details: 'Your operating system is ${_platform.operatingSystem}', - ); - } - return (await _channel.invokeMethod('authenticate', args)) ?? false; - } - - /// Returns true if auth was cancelled successfully. - /// This api only works for Android. - /// Returns false if there was some error or no auth in progress. - /// - /// Returns [Future] bool true or false: - Future stopAuthentication() async { - if (_platform.isAndroid) { - return await _channel.invokeMethod('stopAuthentication') ?? false; - } - return true; - } - - /// Returns true if device is capable of checking biometrics - /// - /// Returns a [Future] bool true or false: - Future get canCheckBiometrics async => - (await _channel.invokeListMethod('getAvailableBiometrics'))! - .isNotEmpty; - - /// Returns true if device is capable of checking biometrics or is able to - /// fail over to device credentials. - /// - /// Returns a [Future] bool true or false: - Future isDeviceSupported() async => - (await _channel.invokeMethod('isDeviceSupported')) ?? false; - - /// Returns a list of enrolled biometrics - /// - /// Returns a [Future] List with the following possibilities: - /// - BiometricType.face - /// - BiometricType.fingerprint - /// - BiometricType.iris (not yet implemented) - Future> getAvailableBiometrics() async { - final List result = (await _channel.invokeListMethod( - 'getAvailableBiometrics', - )) ?? - []; - final List biometrics = []; - for (final String value in result) { - switch (value) { - case 'face': - biometrics.add(BiometricType.face); - break; - case 'fingerprint': - biometrics.add(BiometricType.fingerprint); - break; - case 'iris': - biometrics.add(BiometricType.iris); - break; - case 'undefined': - break; - } - } - return biometrics; - } -} +export 'package:local_auth/src/local_auth.dart' show LocalAuthentication; +export 'package:local_auth_platform_interface/types/auth_options.dart' + show AuthenticationOptions; +export 'package:local_auth_platform_interface/types/biometric_type.dart' + show BiometricType; diff --git a/packages/local_auth/local_auth/lib/src/local_auth.dart b/packages/local_auth/local_auth/lib/src/local_auth.dart new file mode 100644 index 000000000000..77db4d57f018 --- /dev/null +++ b/packages/local_auth/local_auth/lib/src/local_auth.dart @@ -0,0 +1,75 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This is a temporary ignore to allow us to land a new set of linter rules in a +// series of manageable patches instead of one gigantic PR. It disables some of +// the new lints that are already failing on this plugin, for this plugin. It +// should be deleted and the failing lints addressed as soon as possible. +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:local_auth/src/types/error_codes.dart'; +import 'package:local_auth_android/local_auth_android.dart'; +import 'package:local_auth_ios/local_auth_ios.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; + +/// A Flutter plugin for authenticating the user identity locally. +class LocalAuthentication { + /// Authenticates the user with biometrics available on the device while also + /// allowing the user to use device authentication - pin, pattern, passcode. + /// + /// Returns true if the user successfully authenticated, false otherwise. + /// + /// [localizedReason] is the message to show to user while prompting them + /// for authentication. This is typically along the lines of: 'Authenticate + /// to access MyApp.'. This must not be empty. + /// + /// Provide [authMessages] if you want to + /// customize messages in the dialogs. + /// + /// Provide [options] for configuring further authentication related options. + /// + /// Throws a [PlatformException] if there were technical problems with local + /// authentication (e.g. lack of relevant hardware). This might throw + /// [PlatformException] with error code [otherOperatingSystem] on the iOS + /// simulator. + Future authenticate( + {required String localizedReason, + Iterable authMessages = const [ + IOSAuthMessages(), + AndroidAuthMessages() + ], + AuthenticationOptions options = const AuthenticationOptions()}) { + return LocalAuthPlatform.instance.authenticate( + localizedReason: localizedReason, + authMessages: authMessages, + options: options, + ); + } + + /// Cancels any in-progress authentication, returning true if auth was + /// cancelled successfully. + /// + /// This API is not supported by all platforms. + /// Returns false if there was some error, no authentication in progress, + /// or the current platform lacks support. + Future stopAuthentication() async { + return LocalAuthPlatform.instance.stopAuthentication(); + } + + /// Returns true if device is capable of checking biometrics. + Future get canCheckBiometrics => + LocalAuthPlatform.instance.deviceSupportsBiometrics(); + + /// Returns true if device is capable of checking biometrics or is able to + /// fail over to device credentials. + Future isDeviceSupported() async => + LocalAuthPlatform.instance.isDeviceSupported(); + + /// Returns a list of enrolled biometrics. + Future> getAvailableBiometrics() => + LocalAuthPlatform.instance.getEnrolledBiometrics(); +} diff --git a/packages/local_auth/local_auth/lib/error_codes.dart b/packages/local_auth/local_auth/lib/src/types/error_codes.dart similarity index 93% rename from packages/local_auth/local_auth/lib/error_codes.dart rename to packages/local_auth/local_auth/lib/src/types/error_codes.dart index bcf15b7b2154..3426099bacbd 100644 --- a/packages/local_auth/local_auth/lib/error_codes.dart +++ b/packages/local_auth/local_auth/lib/src/types/error_codes.dart @@ -15,7 +15,7 @@ const String notEnrolled = 'NotEnrolled'; /// Indicates the device does not have a Touch ID/fingerprint scanner. const String notAvailable = 'NotAvailable'; -/// Indicates the device operating system is not iOS or Android. +/// Indicates the device operating system is unsupported. const String otherOperatingSystem = 'OtherOperatingSystem'; /// Indicates the API lock out due to too many attempts. diff --git a/packages/local_auth/local_auth/pubspec.yaml b/packages/local_auth/local_auth/pubspec.yaml index 78c79f4abce4..fa055fab17f8 100644 --- a/packages/local_auth/local_auth/pubspec.yaml +++ b/packages/local_auth/local_auth/pubspec.yaml @@ -3,27 +3,27 @@ description: Flutter plugin for Android and iOS devices to allow local authentication via fingerprint, touch ID, face ID, passcode, pin, or pattern. repository: https://github.com/flutter/plugins/tree/main/packages/local_auth/local_auth issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.1.11 +version: 2.0.0 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" flutter: plugin: platforms: android: - package: io.flutter.plugins.localauth - pluginClass: LocalAuthPlugin + default_package: local_auth_android ios: - pluginClass: FLTLocalAuthPlugin + default_package: local_auth_ios dependencies: flutter: sdk: flutter - flutter_plugin_android_lifecycle: ^2.0.1 intl: ^0.17.0 - platform: ^3.0.0 + local_auth_android: ^1.0.0 + local_auth_ios: ^1.0.1 + local_auth_platform_interface: ^1.0.1 dev_dependencies: flutter_driver: @@ -32,4 +32,5 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 + mockito: ^5.1.0 + plugin_platform_interface: ^2.1.2 diff --git a/packages/local_auth/local_auth/test/local_auth_test.dart b/packages/local_auth/local_auth/test/local_auth_test.dart index 3de9758f9d0c..069f9fec2966 100644 --- a/packages/local_auth/local_auth/test/local_auth_test.dart +++ b/packages/local_auth/local_auth/test/local_auth_test.dart @@ -2,255 +2,117 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - -import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:local_auth/auth_strings.dart'; import 'package:local_auth/local_auth.dart'; -import 'package:platform/platform.dart'; +import 'package:local_auth_android/local_auth_android.dart'; +import 'package:local_auth_ios/local_auth_ios.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; void main() { - TestWidgetsFlutterBinding.ensureInitialized(); + WidgetsFlutterBinding.ensureInitialized(); + late LocalAuthentication localAuthentication; + late MockLocalAuthPlatform mockLocalAuthPlatform; - group('LocalAuth', () { - const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/local_auth', - ); + setUp(() { + localAuthentication = LocalAuthentication(); + mockLocalAuthPlatform = MockLocalAuthPlatform(); + LocalAuthPlatform.instance = mockLocalAuthPlatform; + }); - final List log = []; - late LocalAuthentication localAuthentication; + test('authenticate calls platform implementation', () { + when(mockLocalAuthPlatform.authenticate( + localizedReason: anyNamed('localizedReason'), + authMessages: anyNamed('authMessages'), + options: anyNamed('options'), + )).thenAnswer((_) async => true); + localAuthentication.authenticate(localizedReason: 'Test Reason'); + verify(mockLocalAuthPlatform.authenticate( + localizedReason: 'Test Reason', + authMessages: [ + const IOSAuthMessages(), + const AndroidAuthMessages(), + ], + options: const AuthenticationOptions(), + )).called(1); + }); + + test('isDeviceSupported calls platform implementation', () { + when(mockLocalAuthPlatform.isDeviceSupported()) + .thenAnswer((_) async => true); + localAuthentication.isDeviceSupported(); + verify(mockLocalAuthPlatform.isDeviceSupported()).called(1); + }); - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) { - log.add(methodCall); - return Future.value(true); - }); - localAuthentication = LocalAuthentication(); - log.clear(); - }); + test('getEnrolledBiometrics calls platform implementation', () { + when(mockLocalAuthPlatform.getEnrolledBiometrics()) + .thenAnswer((_) async => []); + localAuthentication.getAvailableBiometrics(); + verify(mockLocalAuthPlatform.getEnrolledBiometrics()).called(1); + }); - group('With device auth fail over', () { - test('authenticate with no args on Android.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); - await localAuthentication.authenticate( - localizedReason: 'Needs secure', - biometricOnly: true, - ); - expect( - log, - [ - isMethodCall( - 'authenticate', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': true, - 'biometricHint': androidBiometricHint, - 'biometricNotRecognized': androidBiometricNotRecognized, - 'biometricSuccess': androidBiometricSuccess, - 'biometricRequired': androidBiometricRequiredTitle, - 'cancelButton': androidCancelButton, - 'deviceCredentialsRequired': - androidDeviceCredentialsRequiredTitle, - 'deviceCredentialsSetupDescription': - androidDeviceCredentialsSetupDescription, - 'goToSetting': goToSettings, - 'goToSettingDescription': androidGoToSettingsDescription, - 'signInTitle': androidSignInTitle, - }, - ), - ], - ); - }); + test('stopAuthentication calls platform implementation', () { + when(mockLocalAuthPlatform.stopAuthentication()) + .thenAnswer((_) async => true); + localAuthentication.stopAuthentication(); + verify(mockLocalAuthPlatform.stopAuthentication()).called(1); + }); - test('authenticate with no args on iOS.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); - await localAuthentication.authenticate( - localizedReason: 'Needs secure', - biometricOnly: true, - ); - expect( - log, - [ - isMethodCall('authenticate', arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': true, - 'lockOut': iOSLockOut, - 'goToSetting': goToSettings, - 'goToSettingDescriptionIOS': iOSGoToSettingsDescription, - 'okButton': iOSOkButton, - }), - ], - ); - }); + test('canCheckBiometrics returns correct result', () async { + when(mockLocalAuthPlatform.deviceSupportsBiometrics()) + .thenAnswer((_) async => false); + bool? result; + result = await localAuthentication.canCheckBiometrics; + expect(result, false); + when(mockLocalAuthPlatform.deviceSupportsBiometrics()) + .thenAnswer((_) async => true); + result = await localAuthentication.canCheckBiometrics; + expect(result, true); + verify(mockLocalAuthPlatform.deviceSupportsBiometrics()).called(2); + }); +} - test('authenticate with `localizedFallbackTitle` on iOS.', () async { - const IOSAuthMessages iosAuthMessages = - IOSAuthMessages(localizedFallbackTitle: 'Enter PIN'); - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); - await localAuthentication.authenticate( - localizedReason: 'Needs secure', - biometricOnly: true, - iOSAuthStrings: iosAuthMessages, - ); - expect( - log, - [ - isMethodCall('authenticate', arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': true, - 'lockOut': iOSLockOut, - 'goToSetting': goToSettings, - 'goToSettingDescriptionIOS': iOSGoToSettingsDescription, - 'okButton': iOSOkButton, - 'localizedFallbackTitle': 'Enter PIN', - }), - ], - ); - }); +class MockLocalAuthPlatform extends Mock + with MockPlatformInterfaceMixin + implements LocalAuthPlatform { + MockLocalAuthPlatform() { + throwOnMissingStub(this); + } - test('authenticate with no localizedReason on iOS.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); - await expectLater( - localAuthentication.authenticate( - localizedReason: '', - biometricOnly: true, - ), - throwsAssertionError, - ); - }); + @override + Future authenticate({ + required String? localizedReason, + required Iterable? authMessages, + AuthenticationOptions? options = const AuthenticationOptions(), + }) => + super.noSuchMethod( + Invocation.method(#authenticate, [], { + #localizedReason: localizedReason, + #authMessages: authMessages, + #options: options, + }), + returnValue: Future.value(false)) as Future; - test('authenticate with no sensitive transaction.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); - await localAuthentication.authenticate( - localizedReason: 'Insecure', - sensitiveTransaction: false, - useErrorDialogs: false, - biometricOnly: true, - ); - expect( - log, - [ - isMethodCall('authenticate', arguments: { - 'localizedReason': 'Insecure', - 'useErrorDialogs': false, - 'stickyAuth': false, - 'sensitiveTransaction': false, - 'biometricOnly': true, - 'biometricHint': androidBiometricHint, - 'biometricNotRecognized': androidBiometricNotRecognized, - 'biometricSuccess': androidBiometricSuccess, - 'biometricRequired': androidBiometricRequiredTitle, - 'cancelButton': androidCancelButton, - 'deviceCredentialsRequired': - androidDeviceCredentialsRequiredTitle, - 'deviceCredentialsSetupDescription': - androidDeviceCredentialsSetupDescription, - 'goToSetting': goToSettings, - 'goToSettingDescription': androidGoToSettingsDescription, - 'signInTitle': androidSignInTitle, - }), - ], - ); - }); - }); + @override + Future> getEnrolledBiometrics() => + super.noSuchMethod(Invocation.method(#getEnrolledBiometrics, []), + returnValue: Future>.value([])) + as Future>; - group('With biometrics only', () { - test('authenticate with no args on Android.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); - await localAuthentication.authenticate( - localizedReason: 'Needs secure', - ); - expect( - log, - [ - isMethodCall('authenticate', arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': false, - 'biometricHint': androidBiometricHint, - 'biometricNotRecognized': androidBiometricNotRecognized, - 'biometricSuccess': androidBiometricSuccess, - 'biometricRequired': androidBiometricRequiredTitle, - 'cancelButton': androidCancelButton, - 'deviceCredentialsRequired': - androidDeviceCredentialsRequiredTitle, - 'deviceCredentialsSetupDescription': - androidDeviceCredentialsSetupDescription, - 'goToSetting': goToSettings, - 'goToSettingDescription': androidGoToSettingsDescription, - 'signInTitle': androidSignInTitle, - }), - ], - ); - }); + @override + Future isDeviceSupported() => + super.noSuchMethod(Invocation.method(#isDeviceSupported, []), + returnValue: Future.value(false)) as Future; - test('authenticate with no args on iOS.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); - await localAuthentication.authenticate( - localizedReason: 'Needs secure', - ); - expect( - log, - [ - isMethodCall('authenticate', arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': false, - 'lockOut': iOSLockOut, - 'goToSetting': goToSettings, - 'goToSettingDescriptionIOS': iOSGoToSettingsDescription, - 'okButton': iOSOkButton, - }), - ], - ); - }); + @override + Future stopAuthentication() => + super.noSuchMethod(Invocation.method(#stopAuthentication, []), + returnValue: Future.value(false)) as Future; - test('authenticate with no sensitive transaction.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); - await localAuthentication.authenticate( - localizedReason: 'Insecure', - sensitiveTransaction: false, - useErrorDialogs: false, - ); - expect( - log, - [ - isMethodCall('authenticate', arguments: { - 'localizedReason': 'Insecure', - 'useErrorDialogs': false, - 'stickyAuth': false, - 'sensitiveTransaction': false, - 'biometricOnly': false, - 'biometricHint': androidBiometricHint, - 'biometricNotRecognized': androidBiometricNotRecognized, - 'biometricSuccess': androidBiometricSuccess, - 'biometricRequired': androidBiometricRequiredTitle, - 'cancelButton': androidCancelButton, - 'deviceCredentialsRequired': - androidDeviceCredentialsRequiredTitle, - 'deviceCredentialsSetupDescription': - androidDeviceCredentialsSetupDescription, - 'goToSetting': goToSettings, - 'goToSettingDescription': androidGoToSettingsDescription, - 'signInTitle': androidSignInTitle, - }), - ], - ); - }); - }); - }); + @override + Future deviceSupportsBiometrics() => super.noSuchMethod( + Invocation.method(#deviceSupportsBiometrics, []), + returnValue: Future.value(false)) as Future; } diff --git a/packages/local_auth/local_auth_android/AUTHORS b/packages/local_auth/local_auth_android/AUTHORS new file mode 100644 index 000000000000..d5694690c247 --- /dev/null +++ b/packages/local_auth/local_auth_android/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Bodhi Mulders diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md new file mode 100644 index 000000000000..6afcf1ffed07 --- /dev/null +++ b/packages/local_auth/local_auth_android/CHANGELOG.md @@ -0,0 +1,18 @@ +## NEXT + +* Removes unnecessary imports. + +## 1.0.2 + +* Fixes `getEnrolledBiometrics` to match documented behaviour: + Present biometrics that are not enrolled are no longer returned. +* `getEnrolledBiometrics` now only returns `weak` and `strong` biometric types. +* `deviceSupportsBiometrics` now returns the correct value regardless of enrollment state. + +## 1.0.1 + +* Adopts `Object.hash`. + +## 1.0.0 + +* Initial release from migration to federated architecture. diff --git a/packages/local_auth/local_auth_android/LICENSE b/packages/local_auth/local_auth_android/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/local_auth/local_auth_android/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/local_auth/local_auth_android/README.md b/packages/local_auth/local_auth_android/README.md new file mode 100644 index 000000000000..07244912f231 --- /dev/null +++ b/packages/local_auth/local_auth_android/README.md @@ -0,0 +1,11 @@ +# local\_auth\_android + +The Android implementation of [`local_auth`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `local_auth` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/local_auth +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin \ No newline at end of file diff --git a/packages/local_auth/local_auth/android/build.gradle b/packages/local_auth/local_auth_android/android/build.gradle similarity index 100% rename from packages/local_auth/local_auth/android/build.gradle rename to packages/local_auth/local_auth_android/android/build.gradle diff --git a/packages/local_auth/local_auth/android/lint-baseline.xml b/packages/local_auth/local_auth_android/android/lint-baseline.xml similarity index 100% rename from packages/local_auth/local_auth/android/lint-baseline.xml rename to packages/local_auth/local_auth_android/android/lint-baseline.xml diff --git a/packages/local_auth/local_auth/android/settings.gradle b/packages/local_auth/local_auth_android/android/settings.gradle similarity index 100% rename from packages/local_auth/local_auth/android/settings.gradle rename to packages/local_auth/local_auth_android/android/settings.gradle diff --git a/packages/local_auth/local_auth/android/src/main/AndroidManifest.xml b/packages/local_auth/local_auth_android/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/AndroidManifest.xml rename to packages/local_auth/local_auth_android/android/src/main/AndroidManifest.xml diff --git a/packages/local_auth/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java similarity index 100% rename from packages/local_auth/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java rename to packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java diff --git a/packages/local_auth/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java similarity index 91% rename from packages/local_auth/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java rename to packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java index a63e22a512d0..3c5ecad16329 100644 --- a/packages/local_auth/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java @@ -11,7 +11,6 @@ import android.app.KeyguardManager; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.hardware.fingerprint.FingerprintManager; import android.os.Build; import androidx.annotation.NonNull; @@ -40,7 +39,7 @@ */ @SuppressWarnings("deprecation") public class LocalAuthPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware { - private static final String CHANNEL_NAME = "plugins.flutter.io/local_auth"; + private static final String CHANNEL_NAME = "plugins.flutter.io/local_auth_android"; private static final int LOCK_REQUEST_CODE = 221; private Activity activity; private final AtomicBoolean authInProgress = new AtomicBoolean(false); @@ -101,8 +100,8 @@ public void onMethodCall(MethodCall call, @NonNull final Result result) { case "authenticate": authenticate(call, result); break; - case "getAvailableBiometrics": - getAvailableBiometrics(result); + case "getEnrolledBiometrics": + getEnrolledBiometrics(result); break; case "isDeviceSupported": isDeviceSupported(result); @@ -110,6 +109,9 @@ public void onMethodCall(MethodCall call, @NonNull final Result result) { case "stopAuthentication": stopAuthentication(result); break; + case "deviceSupportsBiometrics": + deviceSupportsBiometrics(result); + break; default: result.notImplemented(); break; @@ -248,42 +250,39 @@ private void stopAuthentication(Result result) { } } + private void deviceSupportsBiometrics(final Result result) { + result.success(hasBiometricHardware()); + } + /* - * Returns biometric types available on device + * Returns enrolled biometric types available on device. */ - private void getAvailableBiometrics(final Result result) { + private void getEnrolledBiometrics(final Result result) { try { if (activity == null || activity.isFinishing()) { result.error("no_activity", "local_auth plugin requires a foreground activity", null); return; } - ArrayList biometrics = getAvailableBiometrics(); + ArrayList biometrics = getEnrolledBiometrics(); result.success(biometrics); } catch (Exception e) { result.error("no_biometrics_available", e.getMessage(), null); } } - private ArrayList getAvailableBiometrics() { + private ArrayList getEnrolledBiometrics() { ArrayList biometrics = new ArrayList<>(); if (activity == null || activity.isFinishing()) { return biometrics; } - PackageManager packageManager = activity.getPackageManager(); - if (Build.VERSION.SDK_INT >= 23) { - if (packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { - biometrics.add("fingerprint"); - } + if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) + == BiometricManager.BIOMETRIC_SUCCESS) { + biometrics.add("weak"); } - if (Build.VERSION.SDK_INT >= 29) { - if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) { - biometrics.add("face"); - } - if (packageManager.hasSystemFeature(PackageManager.FEATURE_IRIS)) { - biometrics.add("iris"); - } + if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) + == BiometricManager.BIOMETRIC_SUCCESS) { + biometrics.add("strong"); } - return biometrics; } @@ -359,4 +358,9 @@ public void onDetachedFromActivity() { final Activity getActivity() { return activity; } + + @VisibleForTesting + void setBiometricManager(BiometricManager biometricManager) { + this.biometricManager = biometricManager; + } } diff --git a/packages/local_auth/local_auth/android/src/main/res/drawable/fingerprint_initial_icon.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_initial_icon.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/drawable/fingerprint_initial_icon.xml rename to packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_initial_icon.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/drawable/fingerprint_success_icon.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_success_icon.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/drawable/fingerprint_success_icon.xml rename to packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_success_icon.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/drawable/fingerprint_warning_icon.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_warning_icon.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/drawable/fingerprint_warning_icon.xml rename to packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_warning_icon.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/drawable/ic_done_white_24dp.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_done_white_24dp.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/drawable/ic_done_white_24dp.xml rename to packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_done_white_24dp.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/drawable/ic_fingerprint_white_24dp.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_fingerprint_white_24dp.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/drawable/ic_fingerprint_white_24dp.xml rename to packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_fingerprint_white_24dp.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/drawable/ic_priority_high_white_24dp.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_priority_high_white_24dp.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/drawable/ic_priority_high_white_24dp.xml rename to packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_priority_high_white_24dp.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/layout/go_to_setting.xml b/packages/local_auth/local_auth_android/android/src/main/res/layout/go_to_setting.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/layout/go_to_setting.xml rename to packages/local_auth/local_auth_android/android/src/main/res/layout/go_to_setting.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/layout/scan_fp.xml b/packages/local_auth/local_auth_android/android/src/main/res/layout/scan_fp.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/layout/scan_fp.xml rename to packages/local_auth/local_auth_android/android/src/main/res/layout/scan_fp.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/values/colors.xml b/packages/local_auth/local_auth_android/android/src/main/res/values/colors.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/values/colors.xml rename to packages/local_auth/local_auth_android/android/src/main/res/values/colors.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/values/dimens.xml b/packages/local_auth/local_auth_android/android/src/main/res/values/dimens.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/values/dimens.xml rename to packages/local_auth/local_auth_android/android/src/main/res/values/dimens.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/values/styles.xml b/packages/local_auth/local_auth_android/android/src/main/res/values/styles.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/values/styles.xml rename to packages/local_auth/local_auth_android/android/src/main/res/values/styles.xml diff --git a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java new file mode 100644 index 000000000000..5fbda46b984f --- /dev/null +++ b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java @@ -0,0 +1,230 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.localauth; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; +import androidx.biometric.BiometricManager; +import androidx.lifecycle.Lifecycle; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import java.util.ArrayList; +import java.util.Collections; +import org.junit.Test; + +public class LocalAuthTest { + @Test + public void isDeviceSupportedReturnsFalse() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + plugin.onMethodCall(new MethodCall("isDeviceSupported", null), mockResult); + verify(mockResult).success(false); + } + + @Test + public void deviceSupportsBiometrics_returnsTrueForPresentNonEnrolledBiometrics() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate()) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED); + plugin.setBiometricManager(mockBiometricManager); + plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult); + verify(mockResult).success(true); + } + + @Test + public void deviceSupportsBiometrics_returnsTrueForPresentEnrolledBiometrics() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate()).thenReturn(BiometricManager.BIOMETRIC_SUCCESS); + plugin.setBiometricManager(mockBiometricManager); + plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult); + verify(mockResult).success(true); + } + + @Test + public void deviceSupportsBiometrics_returnsFalseForNoBiometricHardware() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate()) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE); + plugin.setBiometricManager(mockBiometricManager); + plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult); + verify(mockResult).success(false); + } + + @Test + public void deviceSupportsBiometrics_returnsFalseForNullBiometricManager() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + plugin.setBiometricManager(null); + plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult); + verify(mockResult).success(false); + } + + @Test + public void onDetachedFromActivity_ShouldReleaseActivity() { + final Activity mockActivity = mock(Activity.class); + final ActivityPluginBinding mockActivityBinding = mock(ActivityPluginBinding.class); + when(mockActivityBinding.getActivity()).thenReturn(mockActivity); + + Context mockContext = mock(Context.class); + when(mockActivity.getBaseContext()).thenReturn(mockContext); + + final HiddenLifecycleReference mockLifecycleReference = mock(HiddenLifecycleReference.class); + when(mockActivityBinding.getLifecycle()).thenReturn(mockLifecycleReference); + + final Lifecycle mockLifecycle = mock(Lifecycle.class); + when(mockLifecycleReference.getLifecycle()).thenReturn(mockLifecycle); + + final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class); + final FlutterEngine mockFlutterEngine = mock(FlutterEngine.class); + when(mockPluginBinding.getFlutterEngine()).thenReturn(mockFlutterEngine); + + DartExecutor mockDartExecutor = mock(DartExecutor.class); + when(mockFlutterEngine.getDartExecutor()).thenReturn(mockDartExecutor); + + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + plugin.onAttachedToEngine(mockPluginBinding); + plugin.onAttachedToActivity(mockActivityBinding); + assertNotNull(plugin.getActivity()); + + plugin.onDetachedFromActivity(); + assertNull(plugin.getActivity()); + } + + @Test + public void getEnrolledBiometrics_shouldReturnError_whenNoActivity() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult); + verify(mockResult) + .error("no_activity", "local_auth plugin requires a foreground activity", null); + } + + @Test + public void getEnrolledBiometrics_shouldReturnError_whenFinishingActivity() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final Activity mockActivity = buildMockActivity(); + when(mockActivity.isFinishing()).thenReturn(true); + setPluginActivity(plugin, mockActivity); + + plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult); + verify(mockResult) + .error("no_activity", "local_auth plugin requires a foreground activity", null); + } + + @Test + public void getEnrolledBiometrics_shouldReturnEmptyList_withoutHardwarePresent() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + setPluginActivity(plugin, buildMockActivity()); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate(anyInt())) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE); + plugin.setBiometricManager(mockBiometricManager); + + plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult); + verify(mockResult).success(Collections.emptyList()); + } + + @Test + public void getEnrolledBiometrics_shouldReturnEmptyList_withNoMethodsEnrolled() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + setPluginActivity(plugin, buildMockActivity()); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate(anyInt())) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED); + plugin.setBiometricManager(mockBiometricManager); + + plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult); + verify(mockResult).success(Collections.emptyList()); + } + + @Test + public void getEnrolledBiometrics_shouldOnlyAddEnrolledBiometrics() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + setPluginActivity(plugin, buildMockActivity()); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED); + plugin.setBiometricManager(mockBiometricManager); + + plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult); + verify(mockResult) + .success( + new ArrayList() { + { + add("weak"); + } + }); + } + + @Test + public void getEnrolledBiometrics_shouldAddStrongBiometrics() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + setPluginActivity(plugin, buildMockActivity()); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); + plugin.setBiometricManager(mockBiometricManager); + + plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult); + verify(mockResult) + .success( + new ArrayList() { + { + add("weak"); + add("strong"); + } + }); + } + + private Activity buildMockActivity() { + final Activity mockActivity = mock(Activity.class); + final Context mockContext = mock(Context.class); + when(mockActivity.getBaseContext()).thenReturn(mockContext); + when(mockActivity.getApplicationContext()).thenReturn(mockContext); + return mockActivity; + } + + private void setPluginActivity(LocalAuthPlugin plugin, Activity activity) { + final HiddenLifecycleReference mockLifecycleReference = mock(HiddenLifecycleReference.class); + final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class); + final ActivityPluginBinding mockActivityBinding = mock(ActivityPluginBinding.class); + final FlutterEngine mockFlutterEngine = mock(FlutterEngine.class); + final DartExecutor mockDartExecutor = mock(DartExecutor.class); + when(mockPluginBinding.getFlutterEngine()).thenReturn(mockFlutterEngine); + when(mockFlutterEngine.getDartExecutor()).thenReturn(mockDartExecutor); + when(mockActivityBinding.getActivity()).thenReturn(activity); + when(mockActivityBinding.getLifecycle()).thenReturn(mockLifecycleReference); + plugin.onAttachedToEngine(mockPluginBinding); + plugin.onAttachedToActivity(mockActivityBinding); + } +} diff --git a/packages/local_auth/local_auth_android/example/README.md b/packages/local_auth/local_auth_android/example/README.md new file mode 100644 index 000000000000..a4a6091c9ba6 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/README.md @@ -0,0 +1,8 @@ +# local_auth_example + +Demonstrates how to use the local_auth plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/local_auth/local_auth_android/example/android/app/build.gradle b/packages/local_auth/local_auth_android/example/android/app/build.gradle new file mode 100644 index 000000000000..d1cef4bf53a9 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/app/build.gradle @@ -0,0 +1,58 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 31 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "io.flutter.plugins.localauthexample" + minSdkVersion 16 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/packages/local_auth/local_auth_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/local_auth_android/example/android/app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..186b71557c50 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/local_auth/local_auth_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/local_auth/local_auth_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/local_auth/local_auth_android/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java b/packages/local_auth/local_auth_android/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java new file mode 100644 index 000000000000..68c22371d7dd --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.localauth; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterFragmentActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterFragmentActivityTest { + @Rule + public ActivityTestRule rule = + new ActivityTestRule<>(FlutterFragmentActivity.class); +} diff --git a/packages/local_auth/local_auth_android/example/android/app/src/main/AndroidManifest.xml b/packages/local_auth/local_auth_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..8c091772107a --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..17987b79bb8a Binary files /dev/null and b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..09d4391482be Binary files /dev/null and b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/local_auth/local_auth_android/example/android/build.gradle b/packages/local_auth/local_auth_android/example/android/build.gradle new file mode 100644 index 000000000000..54c943621de5 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.1' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/local_auth/local_auth_android/example/android/gradle.properties b/packages/local_auth/local_auth_android/example/android/gradle.properties new file mode 100644 index 000000000000..7fe61a74cee0 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1024m +android.useAndroidX=true +android.enableJetifier=true +android.enableR8=true diff --git a/packages/local_auth/local_auth_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/local_auth_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..cd9fe1c68282 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Jan 03 14:07:08 CST 2021 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip diff --git a/packages/local_auth/local_auth_android/example/android/settings.gradle b/packages/local_auth/local_auth_android/example/android/settings.gradle new file mode 100644 index 000000000000..115da6cb4f4d --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withInputStream { stream -> plugins.load(stream) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/packages/local_auth/local_auth_android/example/android/settings_aar.gradle b/packages/local_auth/local_auth_android/example/android/settings_aar.gradle new file mode 100644 index 000000000000..e7b4def49cb5 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/packages/local_auth/local_auth_android/example/integration_test/local_auth_test.dart b/packages/local_auth/local_auth_android/example/integration_test/local_auth_test.dart new file mode 100644 index 000000000000..1dfc0ae7a6d6 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/integration_test/local_auth_test.dart @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'package:local_auth_android/local_auth_android.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('canCheckBiometrics', (WidgetTester tester) async { + expect( + LocalAuthAndroid().getEnrolledBiometrics(), + completion(isList), + ); + }); +} diff --git a/packages/local_auth/local_auth_android/example/lib/main.dart b/packages/local_auth/local_auth_android/example/lib/main.dart new file mode 100644 index 000000000000..29b1d66440eb --- /dev/null +++ b/packages/local_auth/local_auth_android/example/lib/main.dart @@ -0,0 +1,239 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:local_auth_android/local_auth_android.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + _SupportState _supportState = _SupportState.unknown; + bool? _deviceSupportsBiometrics; + List? _enrolledBiometrics; + String _authorized = 'Not Authorized'; + bool _isAuthenticating = false; + + @override + void initState() { + super.initState(); + LocalAuthPlatform.instance.isDeviceSupported().then( + (bool isSupported) => setState(() => _supportState = isSupported + ? _SupportState.supported + : _SupportState.unsupported), + ); + } + + Future _checkBiometrics() async { + late bool deviceSupportsBiometrics; + try { + deviceSupportsBiometrics = + await LocalAuthPlatform.instance.deviceSupportsBiometrics(); + } on PlatformException catch (e) { + deviceSupportsBiometrics = false; + print(e); + } + if (!mounted) { + return; + } + + setState(() { + _deviceSupportsBiometrics = deviceSupportsBiometrics; + }); + } + + Future _getEnrolledBiometrics() async { + late List availableBiometrics; + try { + availableBiometrics = + await LocalAuthPlatform.instance.getEnrolledBiometrics(); + } on PlatformException catch (e) { + availableBiometrics = []; + print(e); + } + if (!mounted) { + return; + } + + setState(() { + _enrolledBiometrics = availableBiometrics; + }); + } + + Future _authenticate() async { + bool authenticated = false; + try { + setState(() { + _isAuthenticating = true; + _authorized = 'Authenticating'; + }); + authenticated = await LocalAuthPlatform.instance.authenticate( + localizedReason: 'Let OS determine authentication method', + authMessages: [const AndroidAuthMessages()], + options: const AuthenticationOptions( + useErrorDialogs: true, + stickyAuth: true, + ), + ); + setState(() { + _isAuthenticating = false; + }); + } on PlatformException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + _authorized = 'Error - ${e.message}'; + }); + return; + } + if (!mounted) { + return; + } + + setState( + () => _authorized = authenticated ? 'Authorized' : 'Not Authorized'); + } + + Future _authenticateWithBiometrics() async { + bool authenticated = false; + try { + setState(() { + _isAuthenticating = true; + _authorized = 'Authenticating'; + }); + authenticated = await LocalAuthPlatform.instance.authenticate( + localizedReason: + 'Scan your fingerprint (or face or whatever) to authenticate', + authMessages: [const AndroidAuthMessages()], + options: const AuthenticationOptions( + useErrorDialogs: true, + stickyAuth: true, + biometricOnly: true, + ), + ); + setState(() { + _isAuthenticating = false; + _authorized = 'Authenticating'; + }); + } on PlatformException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + _authorized = 'Error - ${e.message}'; + }); + return; + } + if (!mounted) { + return; + } + + final String message = authenticated ? 'Authorized' : 'Not Authorized'; + setState(() { + _authorized = message; + }); + } + + Future _cancelAuthentication() async { + await LocalAuthPlatform.instance.stopAuthentication(); + setState(() => _isAuthenticating = false); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: ListView( + padding: const EdgeInsets.only(top: 30), + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_supportState == _SupportState.unknown) + const CircularProgressIndicator() + else if (_supportState == _SupportState.supported) + const Text('This device is supported') + else + const Text('This device is not supported'), + const Divider(height: 100), + Text( + 'Device supports biometrics: $_deviceSupportsBiometrics\n'), + ElevatedButton( + child: const Text('Check biometrics'), + onPressed: _checkBiometrics, + ), + const Divider(height: 100), + Text('Enrolled biometrics: $_enrolledBiometrics\n'), + ElevatedButton( + child: const Text('Get enrolled biometrics'), + onPressed: _getEnrolledBiometrics, + ), + const Divider(height: 100), + Text('Current State: $_authorized\n'), + if (_isAuthenticating) + ElevatedButton( + onPressed: _cancelAuthentication, + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Text('Cancel Authentication'), + Icon(Icons.cancel), + ], + ), + ) + else + Column( + children: [ + ElevatedButton( + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Text('Authenticate'), + Icon(Icons.perm_device_information), + ], + ), + onPressed: _authenticate, + ), + ElevatedButton( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_isAuthenticating + ? 'Cancel' + : 'Authenticate: biometrics only'), + const Icon(Icons.fingerprint), + ], + ), + onPressed: _authenticateWithBiometrics, + ), + ], + ), + ], + ), + ], + ), + ), + ); + } +} + +enum _SupportState { + unknown, + supported, + unsupported, +} diff --git a/packages/local_auth/local_auth_android/example/pubspec.yaml b/packages/local_auth/local_auth_android/example/pubspec.yaml new file mode 100644 index 000000000000..c07a81d2be3b --- /dev/null +++ b/packages/local_auth/local_auth_android/example/pubspec.yaml @@ -0,0 +1,28 @@ +name: local_auth_android_example +description: Demonstrates how to use the local_auth_android plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + local_auth_android: + # When depending on this package from a real application you should use: + # local_auth_android: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + local_auth_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/local_auth/local_auth_android/example/test_driver/integration_test.dart b/packages/local_auth/local_auth_android/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/local_auth/local_auth_android/lib/local_auth_android.dart b/packages/local_auth/local_auth_android/lib/local_auth_android.dart new file mode 100644 index 000000000000..dfe785cc176f --- /dev/null +++ b/packages/local_auth/local_auth_android/lib/local_auth_android.dart @@ -0,0 +1,80 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:local_auth_android/types/auth_messages_android.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; + +export 'package:local_auth_android/types/auth_messages_android.dart'; +export 'package:local_auth_platform_interface/types/auth_messages.dart'; +export 'package:local_auth_platform_interface/types/auth_options.dart'; +export 'package:local_auth_platform_interface/types/biometric_type.dart'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/local_auth_android'); + +/// The implementation of [LocalAuthPlatform] for Android. +class LocalAuthAndroid extends LocalAuthPlatform { + /// Registers this class as the default instance of [LocalAuthPlatform]. + static void registerWith() { + LocalAuthPlatform.instance = LocalAuthAndroid(); + } + + @override + Future authenticate({ + required String localizedReason, + required Iterable authMessages, + AuthenticationOptions options = const AuthenticationOptions(), + }) async { + assert(localizedReason.isNotEmpty); + final Map args = { + 'localizedReason': localizedReason, + 'useErrorDialogs': options.useErrorDialogs, + 'stickyAuth': options.stickyAuth, + 'sensitiveTransaction': options.sensitiveTransaction, + 'biometricOnly': options.biometricOnly, + }; + args.addAll(const AndroidAuthMessages().args); + for (final AuthMessages messages in authMessages) { + if (messages is AndroidAuthMessages) { + args.addAll(messages.args); + } + } + return (await _channel.invokeMethod('authenticate', args)) ?? false; + } + + @override + Future deviceSupportsBiometrics() async { + return (await _channel.invokeMethod('deviceSupportsBiometrics')) ?? + false; + } + + @override + Future> getEnrolledBiometrics() async { + final List result = (await _channel.invokeListMethod( + 'getEnrolledBiometrics', + )) ?? + []; + final List biometrics = []; + for (final String value in result) { + switch (value) { + case 'weak': + biometrics.add(BiometricType.weak); + break; + case 'strong': + biometrics.add(BiometricType.strong); + break; + } + } + return biometrics; + } + + @override + Future isDeviceSupported() async => + (await _channel.invokeMethod('isDeviceSupported')) ?? false; + + @override + Future stopAuthentication() async => + await _channel.invokeMethod('stopAuthentication') ?? false; +} diff --git a/packages/local_auth/local_auth_android/lib/types/auth_messages_android.dart b/packages/local_auth/local_auth_android/lib/types/auth_messages_android.dart new file mode 100644 index 000000000000..ad901248e63c --- /dev/null +++ b/packages/local_auth/local_auth_android/lib/types/auth_messages_android.dart @@ -0,0 +1,192 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:intl/intl.dart'; +import 'package:local_auth_platform_interface/types/auth_messages.dart'; + +/// Android side authentication messages. +/// +/// Provides default values for all messages. +@immutable +class AndroidAuthMessages extends AuthMessages { + /// Constructs a new instance. + const AndroidAuthMessages({ + this.biometricHint, + this.biometricNotRecognized, + this.biometricRequiredTitle, + this.biometricSuccess, + this.cancelButton, + this.deviceCredentialsRequiredTitle, + this.deviceCredentialsSetupDescription, + this.goToSettingsButton, + this.goToSettingsDescription, + this.signInTitle, + }); + + /// Hint message advising the user how to authenticate with biometrics. + /// Maximum 60 characters. + final String? biometricHint; + + /// Message to let the user know that authentication was failed. + /// Maximum 60 characters. + final String? biometricNotRecognized; + + /// Message shown as a title in a dialog which indicates the user + /// has not set up biometric authentication on their device. + /// Maximum 60 characters. + final String? biometricRequiredTitle; + + /// Message to let the user know that authentication was successful. + /// Maximum 60 characters + final String? biometricSuccess; + + /// Message shown on a button that the user can click to leave the + /// current dialog. + /// Maximum 30 characters. + final String? cancelButton; + + /// Message shown as a title in a dialog which indicates the user + /// has not set up credentials authentication on their device. + /// Maximum 60 characters. + final String? deviceCredentialsRequiredTitle; + + /// Message advising the user to go to the settings and configure + /// device credentials on their device. + final String? deviceCredentialsSetupDescription; + + /// Message shown on a button that the user can click to go to settings pages + /// from the current dialog. + /// Maximum 30 characters. + final String? goToSettingsButton; + + /// Message advising the user to go to the settings and configure + /// biometric on their device. + final String? goToSettingsDescription; + + /// Message shown as a title in a dialog which indicates the user + /// that they need to scan biometric to continue. + /// Maximum 60 characters. + final String? signInTitle; + + @override + Map get args { + return { + 'biometricHint': biometricHint ?? androidBiometricHint, + 'biometricNotRecognized': + biometricNotRecognized ?? androidBiometricNotRecognized, + 'biometricSuccess': biometricSuccess ?? androidBiometricSuccess, + 'biometricRequired': + biometricRequiredTitle ?? androidBiometricRequiredTitle, + 'cancelButton': cancelButton ?? androidCancelButton, + 'deviceCredentialsRequired': deviceCredentialsRequiredTitle ?? + androidDeviceCredentialsRequiredTitle, + 'deviceCredentialsSetupDescription': deviceCredentialsSetupDescription ?? + androidDeviceCredentialsSetupDescription, + 'goToSetting': goToSettingsButton ?? goToSettings, + 'goToSettingDescription': + goToSettingsDescription ?? androidGoToSettingsDescription, + 'signInTitle': signInTitle ?? androidSignInTitle, + }; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AndroidAuthMessages && + runtimeType == other.runtimeType && + biometricHint == other.biometricHint && + biometricNotRecognized == other.biometricNotRecognized && + biometricRequiredTitle == other.biometricRequiredTitle && + biometricSuccess == other.biometricSuccess && + cancelButton == other.cancelButton && + deviceCredentialsRequiredTitle == + other.deviceCredentialsRequiredTitle && + deviceCredentialsSetupDescription == + other.deviceCredentialsSetupDescription && + goToSettingsButton == other.goToSettingsButton && + goToSettingsDescription == other.goToSettingsDescription && + signInTitle == other.signInTitle; + + @override + int get hashCode => Object.hash( + super.hashCode, + biometricHint, + biometricNotRecognized, + biometricRequiredTitle, + biometricSuccess, + cancelButton, + deviceCredentialsRequiredTitle, + deviceCredentialsSetupDescription, + goToSettingsButton, + goToSettingsDescription, + signInTitle); +} + +// Default strings for AndroidAuthMessages. Currently supports English. +// Intl.message must be string literals. + +/// Message shown on a button that the user can click to go to settings pages +/// from the current dialog. +String get goToSettings => Intl.message('Go to settings', + desc: 'Message shown on a button that the user can click to go to ' + 'settings pages from the current dialog. Maximum 30 characters.'); + +/// Hint message advising the user how to authenticate with biometrics. +String get androidBiometricHint => Intl.message('Verify identity', + desc: 'Hint message advising the user how to authenticate with biometrics. ' + 'Maximum 60 characters.'); + +/// Message to let the user know that authentication was failed. +String get androidBiometricNotRecognized => + Intl.message('Not recognized. Try again.', + desc: 'Message to let the user know that authentication was failed. ' + 'Maximum 60 characters.'); + +/// Message to let the user know that authentication was successful. It +String get androidBiometricSuccess => Intl.message('Success', + desc: 'Message to let the user know that authentication was successful. ' + 'Maximum 60 characters.'); + +/// Message shown on a button that the user can click to leave the +/// current dialog. +String get androidCancelButton => Intl.message('Cancel', + desc: 'Message shown on a button that the user can click to leave the ' + 'current dialog. Maximum 30 characters.'); + +/// Message shown as a title in a dialog which indicates the user +/// that they need to scan biometric to continue. +String get androidSignInTitle => Intl.message('Authentication required', + desc: 'Message shown as a title in a dialog which indicates the user ' + 'that they need to scan biometric to continue. Maximum 60 characters.'); + +/// Message shown as a title in a dialog which indicates the user +/// has not set up biometric authentication on their device. +String get androidBiometricRequiredTitle => Intl.message('Biometric required', + desc: 'Message shown as a title in a dialog which indicates the user ' + 'has not set up biometric authentication on their device. ' + 'Maximum 60 characters.'); + +/// Message shown as a title in a dialog which indicates the user +/// has not set up credentials authentication on their device. +String get androidDeviceCredentialsRequiredTitle => + Intl.message('Device credentials required', + desc: 'Message shown as a title in a dialog which indicates the user ' + 'has not set up credentials authentication on their device. ' + 'Maximum 60 characters.'); + +/// Message advising the user to go to the settings and configure +/// device credentials on their device. +String get androidDeviceCredentialsSetupDescription => + Intl.message('Device credentials required', + desc: 'Message advising the user to go to the settings and configure ' + 'device credentials on their device.'); + +/// Message advising the user to go to the settings and configure +/// biometric on their device. +String get androidGoToSettingsDescription => Intl.message( + 'Biometric authentication is not set up on your device. Go to ' + '\'Settings > Security\' to add biometric authentication.', + desc: 'Message advising the user to go to the settings and configure ' + 'biometric on their device.'); diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml new file mode 100644 index 000000000000..aad0b9b92e70 --- /dev/null +++ b/packages/local_auth/local_auth_android/pubspec.yaml @@ -0,0 +1,29 @@ +name: local_auth_android +description: Android implementation of the local_auth plugin. +repository: https://github.com/flutter/plugins/tree/master/packages/local_auth/local_auth_android +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 +version: 1.0.2 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: local_auth + platforms: + android: + package: io.flutter.plugins.localauth + pluginClass: LocalAuthPlugin + dartPluginClass: LocalAuthAndroid + +dependencies: + flutter: + sdk: flutter + flutter_plugin_android_lifecycle: ^2.0.1 + intl: ^0.17.0 + local_auth_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/packages/local_auth/local_auth_android/test/local_auth_test.dart b/packages/local_auth/local_auth_android/test/local_auth_test.dart new file mode 100644 index 000000000000..86e5713f4bd6 --- /dev/null +++ b/packages/local_auth/local_auth_android/test/local_auth_test.dart @@ -0,0 +1,176 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:local_auth_android/local_auth_android.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('LocalAuth', () { + const MethodChannel channel = MethodChannel( + 'plugins.flutter.io/local_auth_android', + ); + + final List log = []; + late LocalAuthAndroid localAuthentication; + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) { + log.add(methodCall); + switch (methodCall.method) { + case 'getEnrolledBiometrics': + return Future>.value(['weak', 'strong']); + default: + return Future.value(true); + } + }); + localAuthentication = LocalAuthAndroid(); + log.clear(); + }); + + test('deviceSupportsBiometrics calls platform', () async { + final bool result = await localAuthentication.deviceSupportsBiometrics(); + + expect( + log, + [ + isMethodCall('deviceSupportsBiometrics', arguments: null), + ], + ); + expect(result, true); + }); + + test('getEnrolledBiometrics calls platform', () async { + final List result = + await localAuthentication.getEnrolledBiometrics(); + + expect( + log, + [ + isMethodCall('getEnrolledBiometrics', arguments: null), + ], + ); + expect(result, [ + BiometricType.weak, + BiometricType.strong, + ]); + }); + + test('isDeviceSupported calls platform', () async { + await localAuthentication.isDeviceSupported(); + expect( + log, + [ + isMethodCall('isDeviceSupported', arguments: null), + ], + ); + }); + + test('stopAuthentication calls platform', () async { + await localAuthentication.stopAuthentication(); + expect( + log, + [ + isMethodCall('stopAuthentication', arguments: null), + ], + ); + }); + + group('With device auth fail over', () { + test('authenticate with no args.', () async { + await localAuthentication.authenticate( + authMessages: [const AndroidAuthMessages()], + localizedReason: 'Needs secure', + options: const AuthenticationOptions(biometricOnly: true), + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': true, + }..addAll(const AndroidAuthMessages().args)), + ], + ); + }); + + test('authenticate with no sensitive transaction.', () async { + await localAuthentication.authenticate( + authMessages: [const AndroidAuthMessages()], + localizedReason: 'Insecure', + options: const AuthenticationOptions( + sensitiveTransaction: false, + useErrorDialogs: false, + biometricOnly: true, + ), + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Insecure', + 'useErrorDialogs': false, + 'stickyAuth': false, + 'sensitiveTransaction': false, + 'biometricOnly': true, + }..addAll(const AndroidAuthMessages().args)), + ], + ); + }); + }); + + group('With biometrics only', () { + test('authenticate with no args.', () async { + await localAuthentication.authenticate( + authMessages: [const AndroidAuthMessages()], + localizedReason: 'Needs secure', + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': false, + }..addAll(const AndroidAuthMessages().args)), + ], + ); + }); + + test('authenticate with no sensitive transaction.', () async { + await localAuthentication.authenticate( + authMessages: [const AndroidAuthMessages()], + localizedReason: 'Insecure', + options: const AuthenticationOptions( + sensitiveTransaction: false, + useErrorDialogs: false, + ), + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Insecure', + 'useErrorDialogs': false, + 'stickyAuth': false, + 'sensitiveTransaction': false, + 'biometricOnly': false, + }..addAll(const AndroidAuthMessages().args)), + ], + ); + }); + }); + }); +} diff --git a/packages/local_auth/local_auth_ios/AUTHORS b/packages/local_auth/local_auth_ios/AUTHORS new file mode 100644 index 000000000000..d5694690c247 --- /dev/null +++ b/packages/local_auth/local_auth_ios/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Bodhi Mulders diff --git a/packages/local_auth/local_auth_ios/CHANGELOG.md b/packages/local_auth/local_auth_ios/CHANGELOG.md new file mode 100644 index 000000000000..ca6ac43eb52f --- /dev/null +++ b/packages/local_auth/local_auth_ios/CHANGELOG.md @@ -0,0 +1,24 @@ +## NEXT + +* Removes unnecessary imports. + +## 1.0.4 + +* Fixes `deviceSupportsBiometrics` to return true when biometric hardware + is available but not enrolled. + +## 1.0.3 + +* Adopts `Object.hash`. + +## 1.0.2 + +* Adds support `localizedFallbackTitle` in authenticateWithBiometrics on iOS. + +## 1.0.1 + +* BREAKING CHANGE: Changes `stopAuthentication` to always return false instead of throwing an error. + +## 1.0.0 + +* Initial release from migration to federated architecture. diff --git a/packages/local_auth/local_auth_ios/LICENSE b/packages/local_auth/local_auth_ios/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/local_auth/local_auth_ios/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/local_auth/local_auth_ios/README.md b/packages/local_auth/local_auth_ios/README.md new file mode 100644 index 000000000000..d9f40436b617 --- /dev/null +++ b/packages/local_auth/local_auth_ios/README.md @@ -0,0 +1,11 @@ +# local\_auth\_ios + +The iOS implementation of [`local_auth`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `local_auth` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/local_auth +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/local_auth/local_auth_ios/example/README.md b/packages/local_auth/local_auth_ios/example/README.md new file mode 100644 index 000000000000..a4a6091c9ba6 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/README.md @@ -0,0 +1,8 @@ +# local_auth_example + +Demonstrates how to use the local_auth plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/local_auth/local_auth_ios/example/integration_test/local_auth_test.dart b/packages/local_auth/local_auth_ios/example/integration_test/local_auth_test.dart new file mode 100644 index 000000000000..d73cfd6aa625 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/integration_test/local_auth_test.dart @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'package:local_auth_ios/local_auth_ios.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('canCheckBiometrics', (WidgetTester tester) async { + expect( + LocalAuthIOS().getEnrolledBiometrics(), + completion(isList), + ); + }); +} diff --git a/packages/local_auth/local_auth_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/local_auth/local_auth_ios/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 9.0 + + diff --git a/packages/local_auth/local_auth_ios/example/ios/Flutter/Debug.xcconfig b/packages/local_auth/local_auth_ios/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..e8efba114687 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/local_auth/local_auth_ios/example/ios/Flutter/Release.xcconfig b/packages/local_auth/local_auth_ios/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..399e9340e6f6 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/local_auth/local_auth_ios/example/ios/Podfile b/packages/local_auth/local_auth_ios/example/ios/Podfile new file mode 100644 index 000000000000..ee8f1d9ec3ef --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + + pod 'OCMock','3.5' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..cbf16eef4060 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,630 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 0CCCD07A2CE24E13C9C1EEA4 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9D274A3F79473B1549B2BBD5 /* libPods-Runner.a */; }; + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3398D2E426164AD8005A052F /* FLTLocalAuthPluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3398D2E326164AD8005A052F /* FLTLocalAuthPluginTests.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 691CB38B382734AF80FBCA4C /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBFA21B380E07A3A585383D /* libPods-RunnerTests.a */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 3398D2D226163948005A052F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3398D2CD26163948005A052F /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3398D2D126163948005A052F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3398D2DC261649CD005A052F /* liblocal_auth.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = liblocal_auth.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 3398D2DF26164A03005A052F /* liblocal_auth.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = liblocal_auth.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 3398D2E326164AD8005A052F /* FLTLocalAuthPluginTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTLocalAuthPluginTests.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 658CDD04B21E4EA92F8EF229 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 8D6545CD14E27D6F8299FFD5 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9D274A3F79473B1549B2BBD5 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + ADBFA21B380E07A3A585383D /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + D9B3BCBC68F8928E2907FB87 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + EB36DF6C3F25E00DF4175422 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3398D2CA26163948005A052F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 691CB38B382734AF80FBCA4C /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0CCCD07A2CE24E13C9C1EEA4 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BF11D226680B2E002967F3 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 3398D2E326164AD8005A052F /* FLTLocalAuthPluginTests.m */, + 3398D2D126163948005A052F /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 33BF11D226680B2E002967F3 /* RunnerTests */, + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + F8CC53B854B121315C7319D2 /* Pods */, + E2D5FA899A019BD3E0DB0917 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 3398D2CD26163948005A052F /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + E2D5FA899A019BD3E0DB0917 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3398D2DF26164A03005A052F /* liblocal_auth.a */, + 3398D2DC261649CD005A052F /* liblocal_auth.a */, + 9D274A3F79473B1549B2BBD5 /* libPods-Runner.a */, + ADBFA21B380E07A3A585383D /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + F8CC53B854B121315C7319D2 /* Pods */ = { + isa = PBXGroup; + children = ( + EB36DF6C3F25E00DF4175422 /* Pods-Runner.debug.xcconfig */, + 658CDD04B21E4EA92F8EF229 /* Pods-Runner.release.xcconfig */, + 8D6545CD14E27D6F8299FFD5 /* Pods-RunnerTests.debug.xcconfig */, + D9B3BCBC68F8928E2907FB87 /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3398D2CC26163948005A052F /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3398D2D426163948005A052F /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 9C21D3AD392EA849AEB09231 /* [CP] Check Pods Manifest.lock */, + 3398D2C926163948005A052F /* Sources */, + 3398D2CA26163948005A052F /* Frameworks */, + 3398D2CB26163948005A052F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3398D2D326163948005A052F /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 3398D2CD26163948005A052F /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 98D96A2D1A74AF66E3DD2DBC /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 3398D2CC26163948005A052F = { + CreatedOnToolsVersion = 12.4; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 3398D2CC26163948005A052F /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3398D2CB26163948005A052F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + 98D96A2D1A74AF66E3DD2DBC /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9C21D3AD392EA849AEB09231 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3398D2C926163948005A052F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3398D2E426164AD8005A052F /* FLTLocalAuthPluginTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 3398D2D326163948005A052F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 3398D2D226163948005A052F /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 3398D2D526163948005A052F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8D6545CD14E27D6F8299FFD5 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + 3398D2D626163948005A052F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D9B3BCBC68F8928E2907FB87 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.localAuthExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.localAuthExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3398D2D426163948005A052F /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3398D2D526163948005A052F /* Debug */, + 3398D2D626163948005A052F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..b2af55dd6d37 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/local_auth/local_auth_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/local_auth/local_auth_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/AppDelegate.h b/packages/local_auth/local_auth_ios/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner/AppDelegate.h @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/AppDelegate.m b/packages/local_auth/local_auth_ios/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..30b87969f44a --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner/AppDelegate.m @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d22f10b2ab63 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000000..28c6bf03016f Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000000..f091b6b0bca8 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000000..4cde12118dda Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000000..d0ef06e7edb8 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000000..dcdc2306c285 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000000..c8f9ed8f5cee Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000000..75b2d164a5a9 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000000..c4df70d39da7 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000000..6a84f41e14e2 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000000..d0e1f5853602 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/local_auth/local_auth_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..ebf48f603974 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Base.lproj/Main.storyboard b/packages/local_auth/local_auth_ios/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Info.plist b/packages/local_auth/local_auth_ios/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..f8e0356d0a68 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + local_auth_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + NSFaceIDUsageDescription + App needs to authenticate using faces. + + diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/main.m b/packages/local_auth/local_auth_ios/example/ios/Runner/main.m new file mode 100644 index 000000000000..f143297b30d6 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner/main.m @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "AppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/packages/local_auth/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m similarity index 56% rename from packages/local_auth/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m rename to packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m index 3572524d8991..50dbb1a6907b 100644 --- a/packages/local_auth/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m +++ b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m @@ -10,7 +10,7 @@ #if __has_include() #import #else -@import local_auth; +@import local_auth_ios; #endif // Private API needed for tests. @@ -269,4 +269,209 @@ - (void)testSkippedLocalizedFallbackTitle { [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } +- (void)testDeviceSupportsBiometrics_withEnrolledHardware { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertTrue([result boolValue]); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testDeviceSupportsBiometrics_withNonEnrolledHardware_iOS11 { + if (@available(iOS 11, *)) { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { + // Write error + NSError *__autoreleasing *authError; + [invocation getArgument:&authError atIndex:3]; + *authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil]; + // Write return value + BOOL returnValue = NO; + NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; + [invocation setReturnValue:&nsReturnValue]; + }; + OCMStub([mockAuthContext canEvaluatePolicy:policy + error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) + .andDo(canEvaluatePolicyHandler); + + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertTrue([result boolValue]); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + } +} + +- (void)testDeviceSupportsBiometrics_withNoBiometricHardware { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { + // Write error + NSError *__autoreleasing *authError; + [invocation getArgument:&authError atIndex:3]; + *authError = [NSError errorWithDomain:@"error" code:0 userInfo:nil]; + // Write return value + BOOL returnValue = NO; + NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; + [invocation setReturnValue:&nsReturnValue]; + }; + OCMStub([mockAuthContext canEvaluatePolicy:policy + error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) + .andDo(canEvaluatePolicyHandler); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertFalse([result boolValue]); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testGetEnrolledBiometrics_withFaceID_iOS11 { + if (@available(iOS 11, *)) { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeFaceID); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSArray class]]); + XCTAssertEqual([result count], 1); + XCTAssertEqualObjects(result[0], @"face"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + } +} + +- (void)testGetEnrolledBiometrics_withTouchID_iOS11 { + if (@available(iOS 11, *)) { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeTouchID); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSArray class]]); + XCTAssertEqual([result count], 1); + XCTAssertEqualObjects(result[0], @"fingerprint"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + } +} + +- (void)testGetEnrolledBiometrics_withTouchID_preIOS11 { + if (@available(iOS 11, *)) { + return; + } + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSArray class]]); + XCTAssertEqual([result count], 1); + XCTAssertEqualObjects(result[0], @"fingerprint"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testGetEnrolledBiometrics_withoutEnrolledHardware_iOS11 { + if (@available(iOS 11, *)) { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { + // Write error + NSError *__autoreleasing *authError; + [invocation getArgument:&authError atIndex:3]; + *authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil]; + // Write return value + BOOL returnValue = NO; + NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; + [invocation setReturnValue:&nsReturnValue]; + }; + OCMStub([mockAuthContext canEvaluatePolicy:policy + error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) + .andDo(canEvaluatePolicyHandler); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSArray class]]); + XCTAssertEqual([result count], 0); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + } +} @end diff --git a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/Info.plist b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/local_auth/local_auth_ios/example/lib/main.dart b/packages/local_auth/local_auth_ios/example/lib/main.dart new file mode 100644 index 000000000000..a8bf23b78a52 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/lib/main.dart @@ -0,0 +1,238 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:local_auth_ios/local_auth_ios.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + _SupportState _supportState = _SupportState.unknown; + bool? _canCheckBiometrics; + List? _enrolledBiometrics; + String _authorized = 'Not Authorized'; + bool _isAuthenticating = false; + + @override + void initState() { + super.initState(); + LocalAuthPlatform.instance.isDeviceSupported().then( + (bool isSupported) => setState(() => _supportState = isSupported + ? _SupportState.supported + : _SupportState.unsupported), + ); + } + + Future _checkBiometrics() async { + late bool deviceSupportsBiometrics; + try { + deviceSupportsBiometrics = + await LocalAuthPlatform.instance.deviceSupportsBiometrics(); + } on PlatformException catch (e) { + deviceSupportsBiometrics = false; + print(e); + } + if (!mounted) { + return; + } + + setState(() { + _canCheckBiometrics = deviceSupportsBiometrics; + }); + } + + Future _getEnrolledBiometrics() async { + late List enrolledBiometrics; + try { + enrolledBiometrics = + await LocalAuthPlatform.instance.getEnrolledBiometrics(); + } on PlatformException catch (e) { + enrolledBiometrics = []; + print(e); + } + if (!mounted) { + return; + } + + setState(() { + _enrolledBiometrics = enrolledBiometrics; + }); + } + + Future _authenticate() async { + bool authenticated = false; + try { + setState(() { + _isAuthenticating = true; + _authorized = 'Authenticating'; + }); + authenticated = await LocalAuthPlatform.instance.authenticate( + localizedReason: 'Let OS determine authentication method', + authMessages: [const IOSAuthMessages()], + options: const AuthenticationOptions( + useErrorDialogs: true, + stickyAuth: true, + ), + ); + setState(() { + _isAuthenticating = false; + }); + } on PlatformException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + _authorized = 'Error - ${e.message}'; + }); + return; + } + if (!mounted) { + return; + } + + setState( + () => _authorized = authenticated ? 'Authorized' : 'Not Authorized'); + } + + Future _authenticateWithBiometrics() async { + bool authenticated = false; + try { + setState(() { + _isAuthenticating = true; + _authorized = 'Authenticating'; + }); + authenticated = await LocalAuthPlatform.instance.authenticate( + localizedReason: + 'Scan your fingerprint (or face or whatever) to authenticate', + authMessages: [const IOSAuthMessages()], + options: const AuthenticationOptions( + useErrorDialogs: true, + stickyAuth: true, + biometricOnly: true, + ), + ); + setState(() { + _isAuthenticating = false; + _authorized = 'Authenticating'; + }); + } on PlatformException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + _authorized = 'Error - ${e.message}'; + }); + return; + } + if (!mounted) { + return; + } + + final String message = authenticated ? 'Authorized' : 'Not Authorized'; + setState(() { + _authorized = message; + }); + } + + Future _cancelAuthentication() async { + await LocalAuthPlatform.instance.stopAuthentication(); + setState(() => _isAuthenticating = false); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: ListView( + padding: const EdgeInsets.only(top: 30), + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_supportState == _SupportState.unknown) + const CircularProgressIndicator() + else if (_supportState == _SupportState.supported) + const Text('This device is supported') + else + const Text('This device is not supported'), + const Divider(height: 100), + Text('Device supports biometrics: $_canCheckBiometrics\n'), + ElevatedButton( + child: const Text('Check biometrics'), + onPressed: _checkBiometrics, + ), + const Divider(height: 100), + Text('Enrolled biometrics: $_enrolledBiometrics\n'), + ElevatedButton( + child: const Text('Get enrolled biometrics'), + onPressed: _getEnrolledBiometrics, + ), + const Divider(height: 100), + Text('Current State: $_authorized\n'), + if (_isAuthenticating) + ElevatedButton( + onPressed: _cancelAuthentication, + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Text('Cancel Authentication'), + Icon(Icons.cancel), + ], + ), + ) + else + Column( + children: [ + ElevatedButton( + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Text('Authenticate'), + Icon(Icons.perm_device_information), + ], + ), + onPressed: _authenticate, + ), + ElevatedButton( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_isAuthenticating + ? 'Cancel' + : 'Authenticate: biometrics only'), + const Icon(Icons.fingerprint), + ], + ), + onPressed: _authenticateWithBiometrics, + ), + ], + ), + ], + ), + ], + ), + ), + ); + } +} + +enum _SupportState { + unknown, + supported, + unsupported, +} diff --git a/packages/local_auth/local_auth_ios/example/pubspec.yaml b/packages/local_auth/local_auth_ios/example/pubspec.yaml new file mode 100644 index 000000000000..f83806b9d08e --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/pubspec.yaml @@ -0,0 +1,28 @@ +name: local_auth_ios_example +description: Demonstrates how to use the local_auth_ios plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + local_auth_ios: + # When depending on this package from a real application you should use: + # local_auth: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + local_auth_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/local_auth/local_auth_ios/example/test_driver/integration_test.dart b/packages/local_auth/local_auth_ios/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/local_auth/local_auth/ios/Assets/.gitkeep b/packages/local_auth/local_auth_ios/ios/Assets/.gitkeep similarity index 100% rename from packages/local_auth/local_auth/ios/Assets/.gitkeep rename to packages/local_auth/local_auth_ios/ios/Assets/.gitkeep diff --git a/packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.h b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.h similarity index 100% rename from packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.h rename to packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.h diff --git a/packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m similarity index 89% rename from packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.m rename to packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m index 70113efa00a0..eb7f637f7850 100644 --- a/packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -20,7 +20,7 @@ @implementation FLTLocalAuthPlugin { + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/local_auth" + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/local_auth_ios" binaryMessenger:[registrar messenger]]; FLTLocalAuthPlugin *instance = [[FLTLocalAuthPlugin alloc] init]; [registrar addMethodCallDelegate:instance channel:channel]; @@ -35,8 +35,10 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } else { [self authenticate:call.arguments withFlutterResult:result]; } - } else if ([@"getAvailableBiometrics" isEqualToString:call.method]) { - [self getAvailableBiometrics:result]; + } else if ([@"getEnrolledBiometrics" isEqualToString:call.method]) { + [self getEnrolledBiometrics:result]; + } else if ([@"deviceSupportsBiometrics" isEqualToString:call.method]) { + [self deviceSupportsBiometrics:result]; } else if ([@"isDeviceSupported" isEqualToString:call.method]) { result(@YES); } else { @@ -93,14 +95,41 @@ - (void)alertMessage:(NSString *)message completion:nil]; } -- (void)getAvailableBiometrics:(FlutterResult)result { +- (void)deviceSupportsBiometrics:(FlutterResult)result { + LAContext *context = self.createAuthContext; + NSError *authError = nil; + // Check if authentication with biometrics is possible. + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + error:&authError]) { + if (authError == nil) { + result(@YES); + return; + } + } + // If not, check if it is because no biometrics are enrolled (but still present). + if (authError != nil) { + if (@available(iOS 11, *)) { + if (authError.code == LAErrorBiometryNotEnrolled) { + result(@YES); + return; + } + } else if (authError.code == LAErrorTouchIDNotEnrolled) { + result(@YES); + return; + } + } + + result(@NO); +} + +- (void)getEnrolledBiometrics:(FlutterResult)result { LAContext *context = self.createAuthContext; NSError *authError = nil; NSMutableArray *biometrics = [[NSMutableArray alloc] init]; if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) { if (authError == nil) { - if (@available(iOS 11.0.1, *)) { + if (@available(iOS 11, *)) { if (context.biometryType == LABiometryTypeFaceID) { [biometrics addObject:@"face"]; } else if (context.biometryType == LABiometryTypeTouchID) { @@ -110,8 +139,6 @@ - (void)getAvailableBiometrics:(FlutterResult)result { [biometrics addObject:@"fingerprint"]; } } - } else if (authError.code == LAErrorTouchIDNotEnrolled) { - [biometrics addObject:@"undefined"]; } result(biometrics); } @@ -180,8 +207,8 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success case LAErrorPasscodeNotSet: case LAErrorTouchIDNotAvailable: case LAErrorTouchIDNotEnrolled: - case LAErrorUserFallback: case LAErrorTouchIDLockout: + case LAErrorUserFallback: [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; return; case LAErrorSystemCancel: diff --git a/packages/local_auth/local_auth/ios/local_auth.podspec b/packages/local_auth/local_auth_ios/ios/local_auth_ios.podspec similarity index 89% rename from packages/local_auth/local_auth/ios/local_auth.podspec rename to packages/local_auth/local_auth_ios/ios/local_auth_ios.podspec index 4ab779ad50cd..0828c6085ea2 100644 --- a/packages/local_auth/local_auth/ios/local_auth.podspec +++ b/packages/local_auth/local_auth_ios/ios/local_auth_ios.podspec @@ -2,7 +2,7 @@ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| - s.name = 'local_auth' + s.name = 'local_auth_ios' s.version = '0.0.1' s.summary = 'Flutter Local Auth' s.description = <<-DESC @@ -13,7 +13,7 @@ Downloaded by pub (not CocoaPods). s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/local_auth' } - s.documentation_url = 'https://pub.dev/packages/local_auth' + s.documentation_url = 'https://pub.dev/packages/local_auth_ios' s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' diff --git a/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart b/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart new file mode 100644 index 000000000000..d9df89a656a8 --- /dev/null +++ b/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart @@ -0,0 +1,85 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:local_auth_ios/types/auth_messages_ios.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; + +export 'package:local_auth_ios/types/auth_messages_ios.dart'; +export 'package:local_auth_platform_interface/types/auth_messages.dart'; +export 'package:local_auth_platform_interface/types/auth_options.dart'; +export 'package:local_auth_platform_interface/types/biometric_type.dart'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/local_auth_ios'); + +/// The implementation of [LocalAuthPlatform] for iOS. +class LocalAuthIOS extends LocalAuthPlatform { + /// Registers this class as the default instance of [LocalAuthPlatform]. + static void registerWith() { + LocalAuthPlatform.instance = LocalAuthIOS(); + } + + @override + Future authenticate({ + required String localizedReason, + required Iterable authMessages, + AuthenticationOptions options = const AuthenticationOptions(), + }) async { + assert(localizedReason.isNotEmpty); + final Map args = { + 'localizedReason': localizedReason, + 'useErrorDialogs': options.useErrorDialogs, + 'stickyAuth': options.stickyAuth, + 'sensitiveTransaction': options.sensitiveTransaction, + 'biometricOnly': options.biometricOnly, + }; + args.addAll(const IOSAuthMessages().args); + for (final AuthMessages messages in authMessages) { + if (messages is IOSAuthMessages) { + args.addAll(messages.args); + } + } + return (await _channel.invokeMethod('authenticate', args)) ?? false; + } + + @override + Future deviceSupportsBiometrics() async { + return (await _channel.invokeMethod('deviceSupportsBiometrics')) ?? + false; + } + + @override + Future> getEnrolledBiometrics() async { + final List result = (await _channel.invokeListMethod( + 'getEnrolledBiometrics', + )) ?? + []; + final List biometrics = []; + for (final String value in result) { + switch (value) { + case 'face': + biometrics.add(BiometricType.face); + break; + case 'fingerprint': + biometrics.add(BiometricType.fingerprint); + break; + case 'iris': + biometrics.add(BiometricType.iris); + break; + } + } + return biometrics; + } + + @override + Future isDeviceSupported() async => + (await _channel.invokeMethod('isDeviceSupported')) ?? false; + + /// Always returns false as this method is not supported on iOS. + @override + Future stopAuthentication() async { + return false; + } +} diff --git a/packages/local_auth/local_auth_ios/lib/types/auth_messages_ios.dart b/packages/local_auth/local_auth_ios/lib/types/auth_messages_ios.dart new file mode 100644 index 000000000000..e5173fc4ab4f --- /dev/null +++ b/packages/local_auth/local_auth_ios/lib/types/auth_messages_ios.dart @@ -0,0 +1,107 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:intl/intl.dart'; +import 'package:local_auth_platform_interface/types/auth_messages.dart'; + +/// Class wrapping all authentication messages needed on iOS. +/// Provides default values for all messages. +@immutable +class IOSAuthMessages extends AuthMessages { + /// Constructs a new instance. + const IOSAuthMessages({ + this.lockOut, + this.goToSettingsButton, + this.goToSettingsDescription, + this.cancelButton, + this.localizedFallbackTitle, + }); + + /// Message advising the user to re-enable biometrics on their device. + final String? lockOut; + + /// Message shown on a button that the user can click to go to settings pages + /// from the current dialog. + /// Maximum 30 characters. + final String? goToSettingsButton; + + /// Message advising the user to go to the settings and configure Biometrics + /// for their device. + final String? goToSettingsDescription; + + /// Message shown on a button that the user can click to leave the current + /// dialog. + /// Maximum 30 characters. + final String? cancelButton; + + /// The localized title for the fallback button in the dialog presented to + /// the user during authentication. + final String? localizedFallbackTitle; + + @override + Map get args { + return { + 'lockOut': lockOut ?? iOSLockOut, + 'goToSetting': goToSettingsButton ?? goToSettings, + 'goToSettingDescriptionIOS': + goToSettingsDescription ?? iOSGoToSettingsDescription, + 'okButton': cancelButton ?? iOSOkButton, + if (localizedFallbackTitle != null) + 'localizedFallbackTitle': localizedFallbackTitle!, + }; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is IOSAuthMessages && + runtimeType == other.runtimeType && + lockOut == other.lockOut && + goToSettingsButton == other.goToSettingsButton && + goToSettingsDescription == other.goToSettingsDescription && + cancelButton == other.cancelButton && + localizedFallbackTitle == other.localizedFallbackTitle; + + @override + int get hashCode => Object.hash( + super.hashCode, + lockOut, + goToSettingsButton, + goToSettingsDescription, + cancelButton, + localizedFallbackTitle, + ); +} + +// Default Strings for IOSAuthMessages plugin. Currently supports English. +// Intl.message must be string literals. + +/// Message shown on a button that the user can click to go to settings pages +/// from the current dialog. +String get goToSettings => Intl.message('Go to settings', + desc: 'Message shown on a button that the user can click to go to ' + 'settings pages from the current dialog. Maximum 30 characters.'); + +/// Message advising the user to re-enable biometrics on their device. +/// It shows in a dialog on iOS. +String get iOSLockOut => Intl.message( + 'Biometric authentication is disabled. Please lock and unlock your screen to ' + 'enable it.', + desc: 'Message advising the user to re-enable biometrics on their device.'); + +/// Message advising the user to go to the settings and configure Biometrics +/// for their device. +String get iOSGoToSettingsDescription => Intl.message( + 'Biometric authentication is not set up on your device. Please either enable ' + 'Touch ID or Face ID on your phone.', + desc: + 'Message advising the user to go to the settings and configure Biometrics ' + 'for their device.'); + +/// Message shown on a button that the user can click to leave the current +/// dialog. +String get iOSOkButton => Intl.message('OK', + desc: 'Message showed on a button that the user can click to leave the ' + 'current dialog. Maximum 30 characters.'); diff --git a/packages/local_auth/local_auth_ios/pubspec.yaml b/packages/local_auth/local_auth_ios/pubspec.yaml new file mode 100644 index 000000000000..77ab74d383c8 --- /dev/null +++ b/packages/local_auth/local_auth_ios/pubspec.yaml @@ -0,0 +1,27 @@ +name: local_auth_ios +description: iOS implementation of the local_auth plugin. +repository: https://github.com/flutter/plugins/tree/master/packages/local_auth/local_auth_ios +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 +version: 1.0.4 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: local_auth + platforms: + ios: + pluginClass: FLTLocalAuthPlugin + dartPluginClass: LocalAuthIOS + +dependencies: + flutter: + sdk: flutter + intl: ^0.17.0 + local_auth_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/packages/local_auth/local_auth_ios/test/local_auth_test.dart b/packages/local_auth/local_auth_ios/test/local_auth_test.dart new file mode 100644 index 000000000000..0ad89e52f5ce --- /dev/null +++ b/packages/local_auth/local_auth_ios/test/local_auth_test.dart @@ -0,0 +1,183 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:local_auth_ios/local_auth_ios.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('LocalAuth', () { + const MethodChannel channel = MethodChannel( + 'plugins.flutter.io/local_auth_ios', + ); + + final List log = []; + late LocalAuthIOS localAuthentication; + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) { + log.add(methodCall); + switch (methodCall.method) { + case 'getEnrolledBiometrics': + return Future>.value( + ['face', 'fingerprint', 'iris', 'undefined']); + default: + return Future.value(true); + } + }); + localAuthentication = LocalAuthIOS(); + log.clear(); + }); + + test('deviceSupportsBiometrics calls platform', () async { + final bool result = await localAuthentication.deviceSupportsBiometrics(); + + expect( + log, + [ + isMethodCall('deviceSupportsBiometrics', arguments: null), + ], + ); + expect(result, true); + }); + + test('getEnrolledBiometrics calls platform', () async { + final List result = + await localAuthentication.getEnrolledBiometrics(); + + expect( + log, + [ + isMethodCall('getEnrolledBiometrics', arguments: null), + ], + ); + expect(result, [ + BiometricType.face, + BiometricType.fingerprint, + BiometricType.iris + ]); + }); + + test('isDeviceSupported calls platform', () async { + await localAuthentication.isDeviceSupported(); + + expect( + log, + [ + isMethodCall('isDeviceSupported', arguments: null), + ], + ); + }); + + test('stopAuthentication returns false', () async { + final bool result = await localAuthentication.stopAuthentication(); + expect(result, false); + }); + + group('With device auth fail over', () { + test('authenticate with no args.', () async { + await localAuthentication.authenticate( + authMessages: [const IOSAuthMessages()], + localizedReason: 'Needs secure', + options: const AuthenticationOptions(biometricOnly: true), + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': true, + }..addAll(const IOSAuthMessages().args)), + ], + ); + }); + + test('authenticate with no localizedReason.', () async { + await expectLater( + localAuthentication.authenticate( + authMessages: [const IOSAuthMessages()], + localizedReason: '', + options: const AuthenticationOptions(biometricOnly: true), + ), + throwsAssertionError, + ); + }); + }); + + group('With biometrics only', () { + test('authenticate with no args.', () async { + await localAuthentication.authenticate( + authMessages: [const IOSAuthMessages()], + localizedReason: 'Needs secure', + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': false, + }..addAll(const IOSAuthMessages().args)), + ], + ); + }); + + test('authenticate with `localizedFallbackTitle`', () async { + await localAuthentication.authenticate( + authMessages: [ + const IOSAuthMessages(localizedFallbackTitle: 'Enter PIN'), + ], + localizedReason: 'Needs secure', + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': false, + 'localizedFallbackTitle': 'Enter PIN', + }..addAll(const IOSAuthMessages().args)), + ], + ); + }); + + test('authenticate with no sensitive transaction.', () async { + await localAuthentication.authenticate( + authMessages: [const IOSAuthMessages()], + localizedReason: 'Insecure', + options: const AuthenticationOptions( + sensitiveTransaction: false, + useErrorDialogs: false, + ), + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Insecure', + 'useErrorDialogs': false, + 'stickyAuth': false, + 'sensitiveTransaction': false, + 'biometricOnly': false, + }..addAll(const IOSAuthMessages().args)), + ], + ); + }); + }); + }); +} diff --git a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md index 0d8803f93540..10020be3391f 100644 --- a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md +++ b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md @@ -1,3 +1,21 @@ +## NEXT + +* Removes unnecessary imports. + +## 1.0.3 + +* Fixes regression in the default method channel implementation of + `deviceSupportsBiometrics` from federation that would cause it to return true + only if something is enrolled. + +## 1.0.2 + +* Adopts `Object.hash`. + +## 1.0.1 + +* Export externally used types from local_auth_platform_interface.dart directly. + ## 1.0.0 * Initial release. diff --git a/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart b/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart index c68a3bfb8371..9ded078c3a90 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart @@ -4,9 +4,6 @@ import 'package:flutter/services.dart'; import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; -import 'package:local_auth_platform_interface/types/auth_messages.dart'; -import 'package:local_auth_platform_interface/types/auth_options.dart'; -import 'package:local_auth_platform_interface/types/biometric_type.dart'; const MethodChannel _channel = MethodChannel('plugins.flutter.io/local_auth'); @@ -57,6 +54,8 @@ class DefaultLocalAuthPlatform extends LocalAuthPlatform { biometrics.add(BiometricType.iris); break; case 'undefined': + // Sentinel value for the case when nothing is enrolled, but hardware + // support for biometrics is available. break; } } @@ -65,7 +64,14 @@ class DefaultLocalAuthPlatform extends LocalAuthPlatform { @override Future deviceSupportsBiometrics() async { - return (await getEnrolledBiometrics()).isNotEmpty; + final List availableBiometrics = + (await _channel.invokeListMethod( + 'getAvailableBiometrics', + )) ?? + []; + // If anything, including the 'undefined' sentinel, is returned, then there + // is device support for biometrics. + return availableBiometrics.isNotEmpty; } @override diff --git a/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart b/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart index b909ee90d12b..de652b20f462 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart @@ -3,11 +3,11 @@ // found in the LICENSE file. import 'package:local_auth_platform_interface/default_method_channel_platform.dart'; -import 'package:local_auth_platform_interface/types/auth_messages.dart'; -import 'package:local_auth_platform_interface/types/auth_options.dart'; -import 'package:local_auth_platform_interface/types/biometric_type.dart'; +import 'package:local_auth_platform_interface/types/types.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +export 'package:local_auth_platform_interface/types/types.dart'; + /// The interface that implementations of local_auth must implement. /// /// Platform implementations should extend this class rather than implement it as `local_auth` diff --git a/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart b/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart index c4b646c0b97a..a5af8e73a640 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart @@ -52,9 +52,10 @@ class AuthenticationOptions { biometricOnly == other.biometricOnly; @override - int get hashCode => - useErrorDialogs.hashCode ^ - stickyAuth.hashCode ^ - sensitiveTransaction.hashCode ^ - biometricOnly.hashCode; + int get hashCode => Object.hash( + useErrorDialogs, + stickyAuth, + sensitiveTransaction, + biometricOnly, + ); } diff --git a/packages/local_auth/local_auth_platform_interface/lib/types/types.dart b/packages/local_auth/local_auth_platform_interface/lib/types/types.dart new file mode 100644 index 000000000000..ea43b942cffd --- /dev/null +++ b/packages/local_auth/local_auth_platform_interface/lib/types/types.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'auth_messages.dart'; +export 'auth_options.dart'; +export 'biometric_type.dart'; diff --git a/packages/local_auth/local_auth_platform_interface/pubspec.yaml b/packages/local_auth/local_auth_platform_interface/pubspec.yaml index f04268926ebd..ee6c1e9d6577 100644 --- a/packages/local_auth/local_auth_platform_interface/pubspec.yaml +++ b/packages/local_auth/local_auth_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/local_auth/l issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.0.0 +version: 1.0.3 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,4 +19,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - mockito: ^5.0.0 \ No newline at end of file + mockito: ^5.0.0 diff --git a/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart b/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart index 3853fd84c6fc..824597ab2953 100644 --- a/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart +++ b/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart @@ -2,15 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:local_auth_platform_interface/default_method_channel_platform.dart'; import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; -import 'package:local_auth_platform_interface/types/auth_messages.dart'; -import 'package:local_auth_platform_interface/types/auth_options.dart'; -import 'package:local_auth_platform_interface/types/biometric_type.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -19,9 +14,13 @@ void main() { 'plugins.flutter.io/local_auth', ); - final List log = []; + late List log; late LocalAuthPlatform localAuthentication; + setUp(() async { + log = []; + }); + test( 'DefaultLocalAuthPlatform is registered as the default platform implementation', () async { @@ -32,10 +31,9 @@ void main() { test('getAvailableBiometrics', () async { channel.setMockMethodCallHandler((MethodCall methodCall) { log.add(methodCall); - return Future.value([]); + return Future.value([]); }); localAuthentication = DefaultLocalAuthPlatform(); - log.clear(); await localAuthentication.getEnrolledBiometrics(); expect( log, @@ -45,6 +43,29 @@ void main() { ); }); + test('deviceSupportsBiometrics handles special sentinal value', () async { + // The pre-federation implementation of the platform channels, which the + // default implementation retains compatibility with for the benefit of any + // existing unendorsed implementations, used 'undefined' as a special + // return value from `getAvailableBiometrics` to indicate that nothing was + // enrolled, but that the hardware does support biometrics. + channel.setMockMethodCallHandler((MethodCall methodCall) { + log.add(methodCall); + return Future.value(['undefined']); + }); + + localAuthentication = DefaultLocalAuthPlatform(); + final bool supportsBiometrics = + await localAuthentication.deviceSupportsBiometrics(); + expect(supportsBiometrics, true); + expect( + log, + [ + isMethodCall('getAvailableBiometrics', arguments: null), + ], + ); + }); + group('Boolean returning methods', () { setUp(() { channel.setMockMethodCallHandler((MethodCall methodCall) { @@ -52,7 +73,6 @@ void main() { return Future.value(true); }); localAuthentication = DefaultLocalAuthPlatform(); - log.clear(); }); test('isDeviceSupported', () async { diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index d71ebf70c796..a26a8901d1d7 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Removes unnecessary imports. +* Adds OS version support information to README. + ## 2.0.9 * Updates documentation on README.md. diff --git a/packages/path_provider/path_provider/README.md b/packages/path_provider/path_provider/README.md index 20d888ff8d73..e79234ffc07c 100644 --- a/packages/path_provider/path_provider/README.md +++ b/packages/path_provider/path_provider/README.md @@ -2,10 +2,14 @@ [![pub package](https://img.shields.io/pub/v/path_provider.svg)](https://pub.dev/packages/path_provider) -A Flutter plugin for finding commonly used locations on the filesystem. +A Flutter plugin for finding commonly used locations on the filesystem. Supports Android, iOS, Linux, macOS and Windows. Not all methods are supported on all platforms. +| | Android | iOS | Linux | macOS | Windows | +|-------------|---------|------|-------|--------|-------------| +| **Support** | SDK 16+ | 9.0+ | Any | 10.11+ | Windows 10+ | + ## Usage To use this plugin, add `path_provider` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). @@ -40,4 +44,3 @@ Directories support by platform: With that change, tests should be updated to mock `PathProviderPlatform` rather than `PlatformChannel`. See this `path_provider` [test](https://github.com/flutter/plugins/blob/master/packages/path_provider/path_provider/test/path_provider_test.dart) for an example. - diff --git a/packages/path_provider/path_provider/test/path_provider_test.dart b/packages/path_provider/path_provider/test/path_provider_test.dart index 218861606209..aa6d325574df 100644 --- a/packages/path_provider/path_provider/test/path_provider_test.dart +++ b/packages/path_provider/path_provider/test/path_provider_test.dart @@ -8,7 +8,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:test/fake.dart'; const String kTemporaryPath = 'temporaryPath'; const String kApplicationSupportPath = 'applicationSupportPath'; diff --git a/packages/path_provider/path_provider_android/CHANGELOG.md b/packages/path_provider/path_provider_android/CHANGELOG.md index 7b04e90d5e0f..31f8c81f8a65 100644 --- a/packages/path_provider/path_provider_android/CHANGELOG.md +++ b/packages/path_provider/path_provider_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.13 + +* Fixes typing build warning. + ## 2.0.12 * Returns to using a different platform channel name, undoing the revert in diff --git a/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java b/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java index 278ff58b59dc..6dcf9595ac86 100644 --- a/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java +++ b/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java @@ -17,16 +17,14 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.BinaryMessenger.TaskQueue; 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.MethodCodec; import io.flutter.plugin.common.StandardMethodCodec; import io.flutter.util.PathUtils; import java.io.File; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; @@ -154,25 +152,17 @@ public PathProviderPlugin() {} private void setup(BinaryMessenger messenger, Context context) { String channelName = "plugins.flutter.io/path_provider_android"; - // TODO(gaaclarke): Remove reflection guard when https://github.com/flutter/engine/pull/29147 - // becomes available on the stable branch. + TaskQueue taskQueue = messenger.makeBackgroundTaskQueue(); + try { - Class methodChannelClass = Class.forName("io.flutter.plugin.common.MethodChannel"); - Class taskQueueClass = Class.forName("io.flutter.plugin.common.BinaryMessenger$TaskQueue"); - Method makeBackgroundTaskQueue = messenger.getClass().getMethod("makeBackgroundTaskQueue"); - Object taskQueue = makeBackgroundTaskQueue.invoke(messenger); - Constructor constructor = - methodChannelClass.getConstructor( - BinaryMessenger.class, String.class, MethodCodec.class, taskQueueClass); channel = - constructor.newInstance(messenger, channelName, StandardMethodCodec.INSTANCE, taskQueue); + (MethodChannel) + new MethodChannel(messenger, channelName, StandardMethodCodec.INSTANCE, taskQueue); impl = new PathProviderBackgroundThread(); - Log.d(TAG, "Use TaskQueues."); } catch (Exception ex) { - channel = new MethodChannel(messenger, channelName); - impl = new PathProviderPlatformThread(); - Log.d(TAG, "Don't use TaskQueues."); + Log.e(TAG, "Received exception while setting up PathProviderPlugin", ex); } + this.context = context; channel.setMethodCallHandler(this); } diff --git a/packages/path_provider/path_provider_android/pubspec.yaml b/packages/path_provider/path_provider_android/pubspec.yaml index 63b9330a89f9..93ed9848f75b 100644 --- a/packages/path_provider/path_provider_android/pubspec.yaml +++ b/packages/path_provider/path_provider_android/pubspec.yaml @@ -2,11 +2,11 @@ name: path_provider_android description: Android implementation of the path_provider plugin. repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.12 +version: 2.0.13 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=2.8.1" flutter: plugin: diff --git a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md index 8be2da70c20d..4ed22f09a893 100644 --- a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md +++ b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Removes unnecessary imports. + ## 2.0.3 * Removes dependency on `meta`. diff --git a/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart b/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart index c3e5eccbffba..73e6ab48a585 100644 --- a/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart +++ b/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart @@ -7,8 +7,6 @@ import 'package:flutter/services.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:platform/platform.dart'; -import 'enums.dart'; - /// An implementation of [PathProviderPlatform] that uses method channels. class MethodChannelPathProvider extends PathProviderPlatform { /// The method channel used to interact with the native platform. diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md index e95d56c53e9d..c30d7052320e 100644 --- a/packages/quick_actions/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/quick_actions/CHANGELOG.md @@ -1,6 +1,8 @@ ## NEXT +* Removes unnecessary imports. * Updates minimum Flutter version to 2.8. +* Adds OS version support information to README. ## 0.6.0+10 diff --git a/packages/quick_actions/quick_actions/README.md b/packages/quick_actions/quick_actions/README.md index 46e87fa0b241..10bae164d534 100644 --- a/packages/quick_actions/quick_actions/README.md +++ b/packages/quick_actions/quick_actions/README.md @@ -7,10 +7,13 @@ Quick actions refer to the [eponymous concept](https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/home-screen-actions/) on iOS and to the [App Shortcuts](https://developer.android.com/guide/topics/ui/shortcuts.html) APIs on -Android (introduced in Android 7.1 / API level 25). It is safe to run this plugin -with earlier versions of Android as it will produce a noop. +Android. -## Usage in Dart +| | Android | iOS | +|-------------|-----------|------| +| **Support** | SDK 16+\* | 9.0+ | + +## Usage Initialize the library early in your application's lifecycle by providing a callback, which will then be called whenever the user launches the app via a @@ -40,9 +43,7 @@ Please note, that the `type` argument should be unique within your application name of the native resource (xcassets on iOS or drawable on Android) that the app will display for the quick action. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +### Android -For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). +\* The plugin will compile and run on SDK 16+, but will be a no-op below SDK 25 +(Android 7.1). diff --git a/packages/quick_actions/quick_actions/test/quick_actions_test.dart b/packages/quick_actions/quick_actions/test/quick_actions_test.dart index 09fcc9799c11..2747818ae302 100644 --- a/packages/quick_actions/quick_actions/test/quick_actions_test.dart +++ b/packages/quick_actions/quick_actions/test/quick_actions_test.dart @@ -6,9 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:quick_actions/quick_actions.dart'; -import 'package:quick_actions_platform_interface/platform_interface/quick_actions_platform.dart'; import 'package:quick_actions_platform_interface/quick_actions_platform_interface.dart'; -import 'package:quick_actions_platform_interface/types/shortcut_item.dart'; void main() { group('$QuickActions', () { diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 36e60cc35763..84566e26e2c0 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Adds OS version support information to README. + ## 2.0.13 * Updates documentation on README.md. diff --git a/packages/shared_preferences/shared_preferences/README.md b/packages/shared_preferences/shared_preferences/README.md index d3295ac6f6b9..03975ff021e6 100644 --- a/packages/shared_preferences/shared_preferences/README.md +++ b/packages/shared_preferences/shared_preferences/README.md @@ -3,13 +3,17 @@ [![pub package](https://img.shields.io/pub/v/shared_preferences.svg)](https://pub.dev/packages/shared_preferences) Wraps platform-specific persistent storage for simple data -(NSUserDefaults on iOS and macOS, SharedPreferences on Android, etc.). +(NSUserDefaults on iOS and macOS, SharedPreferences on Android, etc.). Data may be persisted to disk asynchronously, and there is no guarantee that writes will be persisted to disk after returning, so this plugin must not be used for storing critical data. Supported data types are `int`, `double`, `bool`, `String` and `List`. +| | Android | iOS | Linux | macOS | Web | Windows | +|-------------|---------|------|-------|--------|-----|-------------| +| **Support** | SDK 16+ | 9.0+ | Any | 10.11+ | Any | Any | + ## Usage To use this plugin, add `shared_preferences` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). @@ -17,24 +21,24 @@ To use this plugin, add `shared_preferences` as a [dependency in your pubspec.ya Here are small examples that show you how to use the API. #### Write data -```dart +```dart // Obtain shared preferences. final prefs = await SharedPreferences.getInstance(); -// Save an integer value to 'counter' key. +// Save an integer value to 'counter' key. await prefs.setInt('counter', 10); -// Save an boolean value to 'repeat' key. +// Save an boolean value to 'repeat' key. await prefs.setBool('repeat', true); -// Save an double value to 'decimal' key. +// Save an double value to 'decimal' key. await prefs.setDouble('decimal', 1.5); -// Save an String value to 'action' key. +// Save an String value to 'action' key. await prefs.setString('action', 'Start'); -// Save an list of strings to 'items' key. +// Save an list of strings to 'items' key. await prefs.setStringList('items', ['Earth', 'Moon', 'Sun']); ``` #### Read data -```dart +```dart // Try reading data from the 'counter' key. If it doesn't exist, returns null. final int? counter = prefs.getInt('counter'); // Try reading data from the 'repeat' key. If it doesn't exist, returns null. @@ -48,8 +52,8 @@ final List? items = prefs.getStringList('items'); ``` #### Remove an entry -```dart -// Remove data for the 'counter' key. +```dart +// Remove data for the 'counter' key. final success = await prefs.remove('counter'); ``` diff --git a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md index 7c86b3c80dc8..34dd631746bb 100644 --- a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Removes unnecessary imports. + ## 2.1.0 * Deprecated `SharedPreferencesWindows.instance` in favor of `SharedPreferencesStorePlatform.instance`. diff --git a/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart index 1d83eead9f25..664048ab98e4 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:shared_preferences_linux/shared_preferences_linux.dart'; diff --git a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md index 1f586a2a9581..0f194de44224 100644 --- a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Removes unnecessary imports. + ## 2.0.3 * Switches to an in-package method channel implementation. diff --git a/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart index 66e3be30ee5d..874ceb4c51a7 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index 1634dcdab0f9..b25956fd5919 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,24 @@ +## NEXT + +* Removes unnecessary imports. + +## 6.1.0 + +* Introduces new `launchUrl` and `canLaunchUrl` APIs; `launch` and `canLaunch` + are now deprecated. These new APIs: + * replace the `String` URL argument with a `Uri`, to prevent common issues + with providing invalid URL strings. + * replace `forceSafariVC` and `forceWebView` with `LaunchMode`, which makes + the API platform-neutral, and standardizes the default behavior between + Android and iOS. + * move web view configuration options into a new `WebViewConfiguration` + object. The default behavior for JavaScript and DOM storage is now enabled + rather than disabled. +* Also deprecates `closeWebView` in favor of `closeInAppWebView` to clarify + that it is specific to the in-app web view launch option. +* Adds OS version support information to README. +* Reorganizes and clarifies README. + ## 6.0.20 * Fixes a typo in `default_package` registration for Windows, macOS, and Linux. diff --git a/packages/url_launcher/url_launcher/README.md b/packages/url_launcher/url_launcher/README.md index a4ffb6241f6c..9c9f0b57e667 100644 --- a/packages/url_launcher/url_launcher/README.md +++ b/packages/url_launcher/url_launcher/README.md @@ -2,8 +2,11 @@ [![pub package](https://img.shields.io/pub/v/url_launcher.svg)](https://pub.dev/packages/url_launcher) -A Flutter plugin for launching a URL. Supports -iOS, Android, web, Windows, macOS, and Linux. +A Flutter plugin for launching a URL. + +| | Android | iOS | Linux | macOS | Web | Windows | +|-------------|---------|------|-------|--------|-----|-------------| +| **Support** | SDK 16+ | 9.0+ | Any | 10.11+ | Any | Windows 10+ | ## Usage @@ -15,14 +18,14 @@ To use this plugin, add `url_launcher` as a [dependency in your pubspec.yaml fil import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; -const String _url = 'https://flutter.dev'; +final Uri _url = Uri.parse('https://flutter.dev'); void main() => runApp( const MaterialApp( home: Material( child: Center( child: RaisedButton( - onPressed: _launchURL, + onPressed: _launchUrl, child: Text('Show Flutter homepage'), ), ), @@ -30,8 +33,8 @@ void main() => runApp( ), ); -void _launchURL() async { - if (!await launch(_url)) throw 'Could not launch $_url'; +void _launchUrl() async { + if (!await launchUrl(_url)) throw 'Could not launch $_url'; } ``` @@ -40,10 +43,10 @@ See the example app for more complex examples. ## Configuration ### iOS -Add any URL schemes passed to `canLaunch` as `LSApplicationQueriesSchemes` entries in your Info.plist file. +Add any URL schemes passed to `canLaunchUrl` as `LSApplicationQueriesSchemes` entries in your Info.plist file. Example: -``` +```xml LSApplicationQueriesSchemes https @@ -56,7 +59,7 @@ See [`-[UIApplication canOpenURL:]`](https://developer.apple.com/documentation/u ### Android Starting from API 30 Android requires package visibility configuration in your -`AndroidManifest.xml` otherwise `canLaunch` will return `false`. A `` +`AndroidManifest.xml` otherwise `canLaunchUrl` will return `false`. A `` element must be added to your manifest as a child of the root element. The snippet below shows an example for an application that uses `https`, `tel`, @@ -91,34 +94,53 @@ for examples of other queries. ## Supported URL schemes -The [`launch`](https://pub.dev/documentation/url_launcher/latest/url_launcher/launch.html) method -takes a string argument containing a URL. This URL -can be formatted using a number of different URL schemes. The supported -URL schemes depend on the underlying platform and installed apps. +The provided URL is passed directly to the host platform for handling. The +supported URL schemes therefore depend on the platform and installed apps. Commonly used schemes include: | Scheme | Example | Action | |:---|:---|:---| -| `https:` | `https://flutter.dev` | Open URL in the default browser | -| `mailto:?subject=&body=` | `mailto:smith@example.org?subject=News&body=New%20plugin` | Create email to in the default email app | -| `tel:` | `tel:+1-555-010-999` | Make a phone call to using the default phone app | -| `sms:` | `sms:5550101234` | Send an SMS message to using the default messaging app | +| `https:` | `https://flutter.dev` | Open `` in the default browser | +| `mailto:?subject=&body=` | `mailto:smith@example.org?subject=News&body=New%20plugin` | Create email to `` in the default email app | +| `tel:` | `tel:+1-555-010-999` | Make a phone call to `` using the default phone app | +| `sms:` | `sms:5550101234` | Send an SMS message to `` using the default messaging app | | `file:` | `file:/home` | Open file or folder using default app association, supported on desktop platforms | More details can be found here for [iOS](https://developer.apple.com/library/content/featuredarticles/iPhoneURLScheme_Reference/Introduction/Introduction.html) and [Android](https://developer.android.com/guide/components/intents-common.html) -**Note**: URL schemes are only supported if there are apps installed on the device that can +URL schemes are only supported if there are apps installed on the device that can support them. For example, iOS simulators don't have a default email or phone apps installed, so can't open `tel:` or `mailto:` links. +### Checking supported schemes + +If you need to know at runtime whether a scheme is guaranteed to work before +using it (for instance, to adjust your UI based on what is available), you can +check with [`canLaunchUrl`](https://pub.dev/documentation/url_launcher/latest/url_launcher/canLaunchUrl.html). + +However, `canLaunchUrl` can return false even if `launchUrl` would work in +some circumstances (in web applications, on mobile without the necessary +configuration as described above, etc.), so in cases where you can provide +fallback behavior it is better to use `launchUrl` directly and handle failure. +For example, a UI button that would have sent feedback email using a `mailto` URL +might instead open a web-based feedback form using an `https` URL on failure, +rather than disabling the button if `canLaunchUrl` returns false for `mailto`. + ### Encoding URLs URLs must be properly encoded, especially when including spaces or other special -characters. This can be done using the +characters. In general this is handled automatically by the [`Uri` class](https://api.dart.dev/dart-core/Uri-class.html). -For example: + +**However**, for any scheme other than `http` or `https`, you should use the +`query` parameter and the `encodeQueryParameters` function shown below rather +than `Uri`'s `queryParameters` constructor argument for any query parameters, +due to [a bug](https://github.com/dart-lang/sdk/issues/43838) in the way `Uri` +encodes query parameters. Using `queryParameters` will result in spaces being +converted to `+` in many cases. + ```dart String? encodeQueryParameters(Map params) { return params.entries @@ -134,43 +156,24 @@ final Uri emailLaunchUri = Uri( }), ); -launch(emailLaunchUri.toString()); +launchUrl(emailLaunchUri); ``` -**Warning**: For any scheme other than `http` or `https`, you should use the -`query` parameter and the `encodeQueryParameters` function shown above rather -than `Uri`'s `queryParameters` constructor argument, due to -[a bug](https://github.com/dart-lang/sdk/issues/43838) in the way `Uri` -encodes query parameters. Using `queryParameters` will result in spaces being -converted to `+` in many cases. +### URLs not handled by `Uri` -### Handling missing URL receivers +In rare cases, you may need to launch a URL that the host system considers +valid, but cannot be expressed by `Uri`. For those cases, alternate APIs using +strings are available by importing `url_launcher_string.dart`. -A particular mobile device may not be able to receive all supported URL schemes. -For example, a tablet may not have a cellular radio and thus no support for -launching a URL using the `sms` scheme, or a device may not have an email app -and thus no support for launching a URL using the `mailto` scheme. +Using these APIs in any other cases is **strongly discouraged**, as providing +invalid URL strings was a very common source of errors with this plugin's +original APIs. -We recommend checking which URL schemes are supported using the -[`canLaunch`](https://pub.dev/documentation/url_launcher/latest/url_launcher/canLaunch.html) -in most cases. If the `canLaunch` method returns false, as a -best practice we suggest adjusting the application UI so that the unsupported -URL is never triggered; for example, if the `mailto` scheme is not supported, a -UI button that would have sent feedback email could be changed to instead open -a web-based feedback form using an `https` URL. +### File scheme handling -## Browser vs In-app Handling -By default, Android opens up a browser when handling URLs. You can pass -`forceWebView: true` parameter to tell the plugin to open a WebView instead. -If you do this for a URL of a page containing JavaScript, make sure to pass in -`enableJavaScript: true`, or else the launch method will not work properly. On -iOS, the default behavior is to open all web URLs within the app. Everything -else is redirected to the app handler. +`file:` scheme can be used on desktop platforms: Windows, macOS, and Linux. -## File scheme handling -`file:` scheme can be used on desktop platforms: `macOS`, `Linux` and `Windows`. - -We recommend checking first whether the directory or file exists before calling `launch`. +We recommend checking first whether the directory or file exists before calling `launchUrl`. Example: ```dart @@ -178,13 +181,21 @@ var filePath = '/path/to/file'; final Uri uri = Uri.file(filePath); if (await File(uri.toFilePath()).exists()) { - if (!await launch(uri.toString())) { + if (!await launchUrl(uri)) { throw 'Could not launch $uri'; } } ``` -### macOS file access configuration +#### macOS file access configuration -If you need to access files outside of your application's sandbox, you will need to have the necessary +If you need to access files outside of your application's sandbox, you will need to have the necessary [entitlements](https://docs.flutter.dev/desktop#entitlements-and-the-app-sandbox). + +## Browser vs in-app Handling + +On some platforms, web URLs can be launched either in an in-app web view, or +in the default browser. The default behavior depends on the platform (see +[`launchUrl`](https://pub.dev/documentation/url_launcher/latest/url_launcher/launchUrl.html) +for details), but a specific mode can be used on supported platforms by +passing a `LaunchMode`. diff --git a/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart b/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart index b527c22390dc..51c2ec892400 100644 --- a/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart @@ -13,18 +13,23 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('canLaunch', (WidgetTester _) async { - expect(await canLaunch('randomstring'), false); + expect( + await canLaunchUrl(Uri(scheme: 'randomscheme', path: 'a_path')), false); // Generally all devices should have some default browser. - expect(await canLaunch('http://flutter.dev'), true); - expect(await canLaunch('https://www.google.com/404'), true); + expect(await canLaunchUrl(Uri(scheme: 'http', host: 'flutter.dev')), true); + expect(await canLaunchUrl(Uri(scheme: 'https', host: 'flutter.dev')), true); // SMS handling is available by default on most platforms. if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) { - expect(await canLaunch('sms:5555555555'), true); + expect(await canLaunchUrl(Uri(scheme: 'sms', path: '5555555555')), true); } - // tel: and mailto: links may not be openable on every device. iOS - // simulators notably can't open these link types. + // Sanity-check legacy API. + // ignore: deprecated_member_use + expect(await canLaunch('randomstring'), false); + // Generally all devices should have some default browser. + // ignore: deprecated_member_use + expect(await canLaunch('https://flutter.dev'), true); }); } diff --git a/packages/url_launcher/url_launcher/example/lib/main.dart b/packages/url_launcher/url_launcher/example/lib/main.dart index a5e38ceecc84..898e80661296 100644 --- a/packages/url_launcher/url_launcher/example/lib/main.dart +++ b/packages/url_launcher/url_launcher/example/lib/main.dart @@ -44,67 +44,62 @@ class _MyHomePageState extends State { void initState() { super.initState(); // Check for phone call support. - canLaunch('tel:123').then((bool result) { + canLaunchUrl(Uri(scheme: 'tel', path: '123')).then((bool result) { setState(() { _hasCallSupport = result; }); }); } - Future _launchInBrowser(String url) async { - if (!await launch( + Future _launchInBrowser(Uri url) async { + if (!await launchUrl( url, - forceSafariVC: false, - forceWebView: false, - headers: {'my_header_key': 'my_header_value'}, + mode: LaunchMode.externalApplication, )) { throw 'Could not launch $url'; } } - Future _launchInWebViewOrVC(String url) async { - if (!await launch( + Future _launchInWebViewOrVC(Uri url) async { + if (!await launchUrl( url, - forceSafariVC: true, - forceWebView: true, - headers: {'my_header_key': 'my_header_value'}, + mode: LaunchMode.inAppWebView, + webViewConfiguration: const WebViewConfiguration( + headers: {'my_header_key': 'my_header_value'}), )) { throw 'Could not launch $url'; } } - Future _launchInWebViewWithJavaScript(String url) async { - if (!await launch( + Future _launchInWebViewWithoutJavaScript(Uri url) async { + if (!await launchUrl( url, - forceSafariVC: true, - forceWebView: true, - enableJavaScript: true, + mode: LaunchMode.inAppWebView, + webViewConfiguration: const WebViewConfiguration(enableJavaScript: false), )) { throw 'Could not launch $url'; } } - Future _launchInWebViewWithDomStorage(String url) async { - if (!await launch( + Future _launchInWebViewWithoutDomStorage(Uri url) async { + if (!await launchUrl( url, - forceSafariVC: true, - forceWebView: true, - enableDomStorage: true, + mode: LaunchMode.inAppWebView, + webViewConfiguration: const WebViewConfiguration(enableDomStorage: false), )) { throw 'Could not launch $url'; } } - Future _launchUniversalLinkIos(String url) async { - final bool nativeAppLaunchSucceeded = await launch( + Future _launchUniversalLinkIos(Uri url) async { + final bool nativeAppLaunchSucceeded = await launchUrl( url, - forceSafariVC: false, - universalLinksOnly: true, + mode: LaunchMode.externalNonBrowserApplication, ); if (!nativeAppLaunchSucceeded) { - await launch( + await launchUrl( url, - forceSafariVC: true, + mode: LaunchMode.inAppWebView, ); } } @@ -118,22 +113,19 @@ class _MyHomePageState extends State { } Future _makePhoneCall(String phoneNumber) async { - // Use `Uri` to ensure that `phoneNumber` is properly URL-encoded. - // Just using 'tel:$phoneNumber' would create invalid URLs in some cases, - // such as spaces in the input, which would cause `launch` to fail on some - // platforms. final Uri launchUri = Uri( scheme: 'tel', path: phoneNumber, ); - await launch(launchUri.toString()); + await launchUrl(launchUri); } @override Widget build(BuildContext context) { // onPressed calls using this URL are not gated on a 'canLaunch' check // because the assumption is that every device can launch a web URL. - const String toLaunch = 'https://www.cylog.org/headers/'; + final Uri toLaunch = + Uri(scheme: 'https', host: 'www.cylog.org', path: 'headers/'); return Scaffold( appBar: AppBar( title: Text(widget.title), @@ -160,9 +152,9 @@ class _MyHomePageState extends State { ? const Text('Make phone call') : const Text('Calling not supported'), ), - const Padding( - padding: EdgeInsets.all(16.0), - child: Text(toLaunch), + Padding( + padding: const EdgeInsets.all(16.0), + child: Text(toLaunch.toString()), ), ElevatedButton( onPressed: () => setState(() { @@ -179,15 +171,15 @@ class _MyHomePageState extends State { ), ElevatedButton( onPressed: () => setState(() { - _launched = _launchInWebViewWithJavaScript(toLaunch); + _launched = _launchInWebViewWithoutJavaScript(toLaunch); }), - child: const Text('Launch in app(JavaScript ON)'), + child: const Text('Launch in app (JavaScript OFF)'), ), ElevatedButton( onPressed: () => setState(() { - _launched = _launchInWebViewWithDomStorage(toLaunch); + _launched = _launchInWebViewWithoutDomStorage(toLaunch); }), - child: const Text('Launch in app(DOM storage ON)'), + child: const Text('Launch in app (DOM storage OFF)'), ), const Padding(padding: EdgeInsets.all(16.0)), ElevatedButton( @@ -203,7 +195,7 @@ class _MyHomePageState extends State { _launched = _launchInWebViewOrVC(toLaunch); Timer(const Duration(seconds: 5), () { print('Closing WebView after 5 seconds...'); - closeWebView(); + closeInAppWebView(); }); }), child: const Text('Launch in app + close after 5 seconds'), diff --git a/packages/url_launcher/url_launcher/lib/src/legacy_api.dart b/packages/url_launcher/url_launcher/lib/src/legacy_api.dart new file mode 100644 index 000000000000..a61b200003a0 --- /dev/null +++ b/packages/url_launcher/url_launcher/lib/src/legacy_api.dart @@ -0,0 +1,154 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +/// Parses the specified URL string and delegates handling of it to the +/// underlying platform. +/// +/// The returned future completes with a [PlatformException] on invalid URLs and +/// schemes which cannot be handled, that is when [canLaunch] would complete +/// with false. +/// +/// By default when [forceSafariVC] is unset, the launcher +/// opens web URLs in the Safari View Controller, anything else is opened +/// using the default handler on the platform. If set to true, it opens the +/// URL in the Safari View Controller. If false, the URL is opened in the +/// default browser of the phone. Note that to work with universal links on iOS, +/// this must be set to false to let the platform's system handle the URL. +/// Set this to false if you want to use the cookies/context of the main browser +/// of the app (such as SSO flows). This setting will nullify [universalLinksOnly] +/// and will always launch a web content in the built-in Safari View Controller regardless +/// if the url is a universal link or not. +/// +/// [universalLinksOnly] is only used in iOS with iOS version >= 10.0. This setting is only validated +/// when [forceSafariVC] is set to false. The default value of this setting is false. +/// By default (when unset), the launcher will either launch the url in a browser (when the +/// url is not a universal link), or launch the respective native app content (when +/// the url is a universal link). When set to true, the launcher will only launch +/// the content if the url is a universal link and the respective app for the universal +/// link is installed on the user's device; otherwise throw a [PlatformException]. +/// +/// [forceWebView] is an Android only setting. If null or false, the URL is +/// always launched with the default browser on device. If set to true, the URL +/// is launched in a WebView. Unlike iOS, browser context is shared across +/// WebViews. +/// [enableJavaScript] is an Android only setting. If true, WebView enable +/// javascript. +/// [enableDomStorage] is an Android only setting. If true, WebView enable +/// DOM storage. +/// [headers] is an Android only setting that adds headers to the WebView. +/// When not using a WebView, the header information is passed to the browser, +/// some Android browsers do not support the [Browser.EXTRA_HEADERS](https://developer.android.com/reference/android/provider/Browser#EXTRA_HEADERS) +/// intent extra and the header information will be lost. +/// [webOnlyWindowName] is an Web only setting . _blank opens the new url in new tab , +/// _self opens the new url in current tab. +/// Default behaviour is to open the url in new tab. +/// +/// Note that if any of the above are set to true but the URL is not a web URL, +/// this will throw a [PlatformException]. +/// +/// [statusBarBrightness] Sets the status bar brightness of the application +/// after opening a link on iOS. Does nothing if no value is passed. This does +/// not handle resetting the previous status bar style. +/// +/// Returns true if launch url is successful; false is only returned when [universalLinksOnly] +/// is set to true and the universal link failed to launch. +@Deprecated('Use launchUrl instead') +Future launch( + String urlString, { + bool? forceSafariVC, + bool forceWebView = false, + bool enableJavaScript = false, + bool enableDomStorage = false, + bool universalLinksOnly = false, + Map headers = const {}, + Brightness? statusBarBrightness, + String? webOnlyWindowName, +}) async { + final Uri? url = Uri.tryParse(urlString.trimLeft()); + final bool isWebURL = + url != null && (url.scheme == 'http' || url.scheme == 'https'); + + if ((forceSafariVC == true || forceWebView == true) && !isWebURL) { + throw PlatformException( + code: 'NOT_A_WEB_SCHEME', + message: 'To use webview or safariVC, you need to pass' + 'in a web URL. This $urlString is not a web URL.'); + } + + /// [true] so that ui is automatically computed if [statusBarBrightness] is set. + bool previousAutomaticSystemUiAdjustment = true; + if (statusBarBrightness != null && + defaultTargetPlatform == TargetPlatform.iOS && + _ambiguate(WidgetsBinding.instance) != null) { + previousAutomaticSystemUiAdjustment = _ambiguate(WidgetsBinding.instance)! + .renderView + .automaticSystemUiAdjustment; + _ambiguate(WidgetsBinding.instance)! + .renderView + .automaticSystemUiAdjustment = false; + SystemChrome.setSystemUIOverlayStyle(statusBarBrightness == Brightness.light + ? SystemUiOverlayStyle.dark + : SystemUiOverlayStyle.light); + } + + final bool result = await UrlLauncherPlatform.instance.launch( + urlString, + useSafariVC: forceSafariVC ?? isWebURL, + useWebView: forceWebView, + enableJavaScript: enableJavaScript, + enableDomStorage: enableDomStorage, + universalLinksOnly: universalLinksOnly, + headers: headers, + webOnlyWindowName: webOnlyWindowName, + ); + + if (statusBarBrightness != null && + _ambiguate(WidgetsBinding.instance) != null) { + _ambiguate(WidgetsBinding.instance)! + .renderView + .automaticSystemUiAdjustment = previousAutomaticSystemUiAdjustment; + } + + return result; +} + +/// Checks whether the specified URL can be handled by some app installed on the +/// device. +/// +/// On some systems, such as recent versions of Android and iOS, this will +/// always return false unless the application has been configuration to allow +/// querying the system for launch support. See +/// [the README](https://pub.dev/packages/url_launcher#configuration) for +/// details. +@Deprecated('Use canLaunchUrl instead') +Future canLaunch(String urlString) async { + return await UrlLauncherPlatform.instance.canLaunch(urlString); +} + +/// Closes the current WebView, if one was previously opened via a call to [launch]. +/// +/// If [launch] was never called, then this call will not have any effect. +/// +/// On Android systems, if [launch] was called without `forceWebView` being set to `true` +/// Or on IOS systems, if [launch] was called without `forceSafariVC` being set to `true`, +/// this call will not do anything either, simply because there is no +/// WebView/SafariViewController available to be closed. +@Deprecated('Use closeInAppWebView instead') +Future closeWebView() async { + return await UrlLauncherPlatform.instance.closeWebView(); +} + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +// TODO(ianh): Remove this once we roll stable in late 2021. +T? _ambiguate(T? value) => value; diff --git a/packages/url_launcher/url_launcher/lib/src/link.dart b/packages/url_launcher/url_launcher/lib/src/link.dart index 016f97daacbf..76cb97748003 100644 --- a/packages/url_launcher/url_launcher/lib/src/link.dart +++ b/packages/url_launcher/url_launcher/lib/src/link.dart @@ -6,10 +6,12 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher_platform_interface/link.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; +import 'types.dart'; +import 'url_launcher_uri.dart'; + /// The function used to push routes to the Flutter framework. @visibleForTesting Future Function(Object?, String) pushRouteToFrameworkFunction = @@ -107,7 +109,8 @@ class DefaultLinkDelegate extends StatelessWidget { } Future _followLink(BuildContext context) async { - if (!link.uri!.hasScheme) { + final Uri url = link.uri!; + if (!url.hasScheme) { // A uri that doesn't have a scheme is an internal route name. In this // case, we push it via Flutter's navigation system instead of letting the // browser handle it. @@ -116,18 +119,18 @@ class DefaultLinkDelegate extends StatelessWidget { return; } - // At this point, we know that the link is external. So we use the `launch` - // API to open the link. - final String urlString = link.uri.toString(); - if (await canLaunch(urlString)) { - await launch( - urlString, - forceSafariVC: _useWebView, - forceWebView: _useWebView, + // At this point, we know that the link is external. So we use the + // `launchUrl` API to open the link. + if (await canLaunchUrl(url)) { + await launchUrl( + url, + mode: _useWebView + ? LaunchMode.inAppWebView + : LaunchMode.externalApplication, ); } else { FlutterError.reportError(FlutterErrorDetails( - exception: 'Could not launch link $urlString', + exception: 'Could not launch link ${url.toString()}', stack: StackTrace.current, library: 'url_launcher', context: ErrorDescription('during launching a link'), diff --git a/packages/url_launcher/url_launcher/lib/src/types.dart b/packages/url_launcher/url_launcher/lib/src/types.dart new file mode 100644 index 000000000000..bcfcb7887b17 --- /dev/null +++ b/packages/url_launcher/url_launcher/lib/src/types.dart @@ -0,0 +1,54 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; + +/// The desired mode to launch a URL. +/// +/// Support for these modes varies by platform. Platforms that do not support +/// the requested mode may substitute another mode. See [launchUrl] for more +/// details. +enum LaunchMode { + /// Leaves the decision of how to launch the URL to the platform + /// implementation. + platformDefault, + + /// Loads the URL in an in-app web view (e.g., Safari View Controller). + inAppWebView, + + /// Passes the URL to the OS to be handled by another application. + externalApplication, + + /// Passes the URL to the OS to be handled by another non-browser application. + externalNonBrowserApplication, +} + +/// Additional configuration options for [LaunchMode.inAppWebView]. +@immutable +class WebViewConfiguration { + /// Creates a new WebViewConfiguration with the given settings. + const WebViewConfiguration({ + this.enableJavaScript = true, + this.enableDomStorage = true, + this.headers = const {}, + }); + + /// Whether or not JavaScript is enabled for the web content. + /// + /// Disabling this may not be supported on all platforms. + final bool enableJavaScript; + + /// Whether or not DOM storage is enabled for the web content. + /// + /// Disabling this may not be supported on all platforms. + final bool enableDomStorage; + + /// Additional headers to pass in the load request. + /// + /// On Android, this may work even when not loading in an in-app web view. + /// When loading in an external browsers, this sets + /// [Browser.EXTRA_HEADERS](https://developer.android.com/reference/android/provider/Browser#EXTRA_HEADERS) + /// Not all browsers support this, so it is not guaranteed to be honored. + final Map headers; +} diff --git a/packages/url_launcher/url_launcher/lib/src/url_launcher_string.dart b/packages/url_launcher/url_launcher/lib/src/url_launcher_string.dart new file mode 100644 index 000000000000..bee2a80a59c0 --- /dev/null +++ b/packages/url_launcher/url_launcher/lib/src/url_launcher_string.dart @@ -0,0 +1,65 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +import 'types.dart'; + +/// String version of [launchUrl]. +/// +/// This should be used only in the very rare case of needing to launch a URL +/// that is considered valid by the host platform, but not by Dart's [Uri] +/// class. In all other cases, use [launchUrl] instead, as that will ensure +/// that you are providing a valid URL. +/// +/// The behavior of this method when passing an invalid URL is entirely +/// platform-specific; no effort is made by the plugin to make the URL valid. +/// Some platforms may provide best-effort interpretation of an invalid URL, +/// others will immediately fail if the URL can't be parsed according to the +/// official standards that define URL formats. +Future launchUrlString( + String urlString, { + LaunchMode mode = LaunchMode.platformDefault, + WebViewConfiguration webViewConfiguration = const WebViewConfiguration(), + String? webOnlyWindowName, +}) async { + final bool isWebURL = + urlString.startsWith('http:') || urlString.startsWith('https:'); + if (mode == LaunchMode.inAppWebView && !isWebURL) { + throw ArgumentError.value(urlString, 'urlString', + 'To use an in-app web view, you must provide an http(s) URL.'); + } + final bool useWebView = mode == LaunchMode.inAppWebView || + (isWebURL && mode == LaunchMode.platformDefault); + + // TODO(stuartmorgan): Create a replacement platform interface method that + // uses something more like the new argument structure, and switch to using + // that, to support launch mode on more platforms. + return await UrlLauncherPlatform.instance.launch( + urlString, + useSafariVC: useWebView, + useWebView: useWebView, + enableJavaScript: webViewConfiguration.enableJavaScript, + enableDomStorage: webViewConfiguration.enableDomStorage, + universalLinksOnly: mode == LaunchMode.externalNonBrowserApplication, + headers: webViewConfiguration.headers, + webOnlyWindowName: webOnlyWindowName, + ); +} + +/// String version of [canLaunchUrl]. +/// +/// This should be used only in the very rare case of needing to check a URL +/// that is considered valid by the host platform, but not by Dart's [Uri] +/// class. In all other cases, use [canLaunchUrl] instead, as that will ensure +/// that you are providing a valid URL. +/// +/// The behavior of this method when passing an invalid URL is entirely +/// platform-specific; no effort is made by the plugin to make the URL valid. +/// Some platforms may provide best-effort interpretation of an invalid URL, +/// others will immediately fail if the URL can't be parsed according to the +/// official standards that define URL formats. +Future canLaunchUrlString(String urlString) async { + return await UrlLauncherPlatform.instance.canLaunch(urlString); +} diff --git a/packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart b/packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart new file mode 100644 index 000000000000..fc33f05e5afb --- /dev/null +++ b/packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart @@ -0,0 +1,88 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +/// Passes [url] to the underlying platform for handling. +/// +/// [mode] support varies significantly by platform: +/// - [LaunchMode.platformDefault] is supported on all platforms: +/// - On iOS and Android, this treats web URLs as +/// [LaunchMode.inAppWebView], and all other URLs as +/// [LaunchMode.externalApplication]. +/// - On Windows, macOS, and Linux this behaves like +/// [LaunchMode.externalApplication]. +/// - On web, this uses `webOnlyWindowName` for web URLs, and behaves like +/// [LaunchMode.externalApplication] for any other content. +/// - [LaunchMode.inAppWebView] is currently only supported on iOS and +/// Android. If a non-web URL is passed with this mode, an [ArgumentError] +/// will be thrown. +/// - [LaunchMode.externalApplication] is supported on all platforms. +/// On iOS, this should be used in cases where sharing the cookies of the +/// user's browser is important, such as SSO flows, since Safari View +/// Controller does not share the browser's context. +/// - [LaunchMode.externalNonBrowserApplication] is supported on iOS 10+. +/// This setting is used to require universal links to open in a non-browser +/// application. +/// +/// For web, [webOnlyWindowName] specifies a target for the launch. This +/// supports the standard special link target names. For example: +/// - "_blank" opens the new URL in a new tab. +/// - "_self" opens the new URL in the current tab. +/// Default behaviour when unset is to open the url in a new tab. +/// +/// Returns true if the URL was launched successful, otherwise either returns +/// false or throws a [PlatformException] depending on the failure. +Future launchUrl( + Uri url, { + LaunchMode mode = LaunchMode.platformDefault, + WebViewConfiguration webViewConfiguration = const WebViewConfiguration(), + String? webOnlyWindowName, +}) async { + final bool isWebURL = url.scheme == 'http' || url.scheme == 'https'; + if (mode == LaunchMode.inAppWebView && !isWebURL) { + throw ArgumentError.value(url, 'url', + 'To use an in-app web view, you must provide an http(s) URL.'); + } + // TODO(stuartmorgan): Use UrlLauncherPlatform directly once a new API + // that better matches these parameters has been added. For now, delegate to + // launchUrlString so that there's only one copy of the parameter translation + // logic. + return await launchUrlString( + url.toString(), + mode: mode, + webViewConfiguration: webViewConfiguration, + webOnlyWindowName: webOnlyWindowName, + ); +} + +/// Checks whether the specified URL can be handled by some app installed on the +/// device. +/// +/// Returns true if it is possible to verify that there is a handler available. +/// A false return value can indicate either that there is no handler available, +/// or that the application does not have permission to check. For example: +/// - On recent versions of Android and iOS, this will always return false +/// unless the application has been configuration to allow +/// querying the system for launch support. See +/// [the README](https://pub.dev/packages/url_launcher#configuration) for +/// details. +/// - On web, this will always return false except for a few specific schemes +/// that are always assumed to be supported (such as http(s)), as web pages +/// are never allowed to query installed applications. +Future canLaunchUrl(Uri url) async { + return await UrlLauncherPlatform.instance.canLaunch(url.toString()); +} + +/// Closes the current in-app web view, if one was previously opened by +/// [launchUrl]. +/// +/// If [launchUrl] was never called with [LaunchMode.inAppWebView], then this +/// call will have no effect. +Future closeInAppWebView() async { + return await UrlLauncherPlatform.instance.closeWebView(); +} diff --git a/packages/url_launcher/url_launcher/lib/url_launcher.dart b/packages/url_launcher/url_launcher/lib/url_launcher.dart index f28c460cce4f..36c7b60fdacd 100644 --- a/packages/url_launcher/url_launcher/lib/url_launcher.dart +++ b/packages/url_launcher/url_launcher/lib/url_launcher.dart @@ -2,150 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; - -/// Parses the specified URL string and delegates handling of it to the -/// underlying platform. -/// -/// The returned future completes with a [PlatformException] on invalid URLs and -/// schemes which cannot be handled, that is when [canLaunch] would complete -/// with false. -/// -/// By default when [forceSafariVC] is unset, the launcher -/// opens web URLs in the Safari View Controller, anything else is opened -/// using the default handler on the platform. If set to true, it opens the -/// URL in the Safari View Controller. If false, the URL is opened in the -/// default browser of the phone. Note that to work with universal links on iOS, -/// this must be set to false to let the platform's system handle the URL. -/// Set this to false if you want to use the cookies/context of the main browser -/// of the app (such as SSO flows). This setting will nullify [universalLinksOnly] -/// and will always launch a web content in the built-in Safari View Controller regardless -/// if the url is a universal link or not. -/// -/// [universalLinksOnly] is only used in iOS with iOS version >= 10.0. This setting is only validated -/// when [forceSafariVC] is set to false. The default value of this setting is false. -/// By default (when unset), the launcher will either launch the url in a browser (when the -/// url is not a universal link), or launch the respective native app content (when -/// the url is a universal link). When set to true, the launcher will only launch -/// the content if the url is a universal link and the respective app for the universal -/// link is installed on the user's device; otherwise throw a [PlatformException]. -/// -/// [forceWebView] is an Android only setting. If null or false, the URL is -/// always launched with the default browser on device. If set to true, the URL -/// is launched in a WebView. Unlike iOS, browser context is shared across -/// WebViews. -/// [enableJavaScript] is an Android only setting. If true, WebView enable -/// javascript. -/// [enableDomStorage] is an Android only setting. If true, WebView enable -/// DOM storage. -/// [headers] is an Android only setting that adds headers to the WebView. -/// When not using a WebView, the header information is passed to the browser, -/// some Android browsers do not support the [Browser.EXTRA_HEADERS](https://developer.android.com/reference/android/provider/Browser#EXTRA_HEADERS) -/// intent extra and the header information will be lost. -/// [webOnlyWindowName] is an Web only setting . _blank opens the new url in new tab , -/// _self opens the new url in current tab. -/// Default behaviour is to open the url in new tab. -/// -/// Note that if any of the above are set to true but the URL is not a web URL, -/// this will throw a [PlatformException]. -/// -/// [statusBarBrightness] Sets the status bar brightness of the application -/// after opening a link on iOS. Does nothing if no value is passed. This does -/// not handle resetting the previous status bar style. -/// -/// Returns true if launch url is successful; false is only returned when [universalLinksOnly] -/// is set to true and the universal link failed to launch. -Future launch( - String urlString, { - bool? forceSafariVC, - bool forceWebView = false, - bool enableJavaScript = false, - bool enableDomStorage = false, - bool universalLinksOnly = false, - Map headers = const {}, - Brightness? statusBarBrightness, - String? webOnlyWindowName, -}) async { - final Uri? url = Uri.tryParse(urlString.trimLeft()); - final bool isWebURL = - url != null && (url.scheme == 'http' || url.scheme == 'https'); - - if ((forceSafariVC == true || forceWebView == true) && !isWebURL) { - throw PlatformException( - code: 'NOT_A_WEB_SCHEME', - message: 'To use webview or safariVC, you need to pass' - 'in a web URL. This $urlString is not a web URL.'); - } - - /// [true] so that ui is automatically computed if [statusBarBrightness] is set. - bool previousAutomaticSystemUiAdjustment = true; - if (statusBarBrightness != null && - defaultTargetPlatform == TargetPlatform.iOS && - _ambiguate(WidgetsBinding.instance) != null) { - previousAutomaticSystemUiAdjustment = _ambiguate(WidgetsBinding.instance)! - .renderView - .automaticSystemUiAdjustment; - _ambiguate(WidgetsBinding.instance)! - .renderView - .automaticSystemUiAdjustment = false; - SystemChrome.setSystemUIOverlayStyle(statusBarBrightness == Brightness.light - ? SystemUiOverlayStyle.dark - : SystemUiOverlayStyle.light); - } - - final bool result = await UrlLauncherPlatform.instance.launch( - urlString, - useSafariVC: forceSafariVC ?? isWebURL, - useWebView: forceWebView, - enableJavaScript: enableJavaScript, - enableDomStorage: enableDomStorage, - universalLinksOnly: universalLinksOnly, - headers: headers, - webOnlyWindowName: webOnlyWindowName, - ); - - if (statusBarBrightness != null && - _ambiguate(WidgetsBinding.instance) != null) { - _ambiguate(WidgetsBinding.instance)! - .renderView - .automaticSystemUiAdjustment = previousAutomaticSystemUiAdjustment; - } - - return result; -} - -/// Checks whether the specified URL can be handled by some app installed on the -/// device. -/// -/// On some systems, such as recent versions of Android and iOS, this will -/// always return false unless the application has been configuration to allow -/// querying the system for launch support. See -/// [the README](https://pub.dev/packages/url_launcher#configuration) for -/// details. -Future canLaunch(String urlString) async { - return await UrlLauncherPlatform.instance.canLaunch(urlString); -} - -/// Closes the current WebView, if one was previously opened via a call to [launch]. -/// -/// If [launch] was never called, then this call will not have any effect. -/// -/// On Android systems, if [launch] was called without `forceWebView` being set to `true` -/// Or on IOS systems, if [launch] was called without `forceSafariVC` being set to `true`, -/// this call will not do anything either, simply because there is no -/// WebView/SafariViewController available to be closed. -Future closeWebView() async { - return await UrlLauncherPlatform.instance.closeWebView(); -} - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. -T? _ambiguate(T? value) => value; +export 'src/legacy_api.dart'; +export 'src/types.dart'; +export 'src/url_launcher_uri.dart'; diff --git a/packages/url_launcher/url_launcher/lib/url_launcher_string.dart b/packages/url_launcher/url_launcher/lib/url_launcher_string.dart new file mode 100644 index 000000000000..b5a12b1e39ca --- /dev/null +++ b/packages/url_launcher/url_launcher/lib/url_launcher_string.dart @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Provides a String-based alterantive to the Uri-based primary API. +// +// This is provided as a separate import because it's much easier to use +// incorrectly, so should require explicit opt-in (to avoid issues such as +// IDE auto-complete to the more error-prone APIs just by importing the +// main API). + +export 'src/types.dart'; +export 'src/url_launcher_string.dart'; diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index feb0a2c9ad95..6803d71032cb 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. Supports web, phone, SMS, and email schemes. repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.0.20 +version: 6.1.0 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/url_launcher/url_launcher/test/link_test.dart b/packages/url_launcher/url_launcher/test/link_test.dart index f7a98a0bf2f0..6242397c5ed7 100644 --- a/packages/url_launcher/url_launcher/test/link_test.dart +++ b/packages/url_launcher/url_launcher/test/link_test.dart @@ -9,7 +9,7 @@ import 'package:url_launcher/link.dart'; import 'package:url_launcher/src/link.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; -import 'mock_url_launcher_platform.dart'; +import 'mocks/mock_url_launcher_platform.dart'; void main() { late MockUrlLauncher mock; @@ -58,8 +58,8 @@ void main() { useSafariVC: false, useWebView: false, universalLinksOnly: false, - enableJavaScript: false, - enableDomStorage: false, + enableJavaScript: true, + enableDomStorage: true, headers: {}, webOnlyWindowName: null, ) @@ -88,8 +88,8 @@ void main() { useSafariVC: true, useWebView: true, universalLinksOnly: false, - enableJavaScript: false, - enableDomStorage: false, + enableJavaScript: true, + enableDomStorage: true, headers: {}, webOnlyWindowName: null, ) diff --git a/packages/url_launcher/url_launcher/test/mock_url_launcher_platform.dart b/packages/url_launcher/url_launcher/test/mocks/mock_url_launcher_platform.dart similarity index 100% rename from packages/url_launcher/url_launcher/test/mock_url_launcher_platform.dart rename to packages/url_launcher/url_launcher/test/mocks/mock_url_launcher_platform.dart diff --git a/packages/url_launcher/url_launcher/test/url_launcher_test.dart b/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart similarity index 98% rename from packages/url_launcher/url_launcher/test/url_launcher_test.dart rename to packages/url_launcher/url_launcher/test/src/legacy_api_test.dart index 4e980cb37253..e94f1847ef51 100644 --- a/packages/url_launcher/url_launcher/test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart @@ -2,16 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show PlatformException; import 'package:flutter_test/flutter_test.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/src/legacy_api.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; -import 'mock_url_launcher_platform.dart'; +import '../mocks/mock_url_launcher_platform.dart'; void main() { final MockUrlLauncher mock = MockUrlLauncher(); diff --git a/packages/url_launcher/url_launcher/test/src/url_launcher_string_test.dart b/packages/url_launcher/url_launcher/test/src/url_launcher_string_test.dart new file mode 100644 index 000000000000..95b2f5c27bf3 --- /dev/null +++ b/packages/url_launcher/url_launcher/test/src/url_launcher_string_test.dart @@ -0,0 +1,279 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:url_launcher/src/types.dart'; +import 'package:url_launcher/src/url_launcher_string.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +import '../mocks/mock_url_launcher_platform.dart'; + +void main() { + final MockUrlLauncher mock = MockUrlLauncher(); + UrlLauncherPlatform.instance = mock; + + group('canLaunchUrlString', () { + test('handles returning true', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setCanLaunchExpectations(urlString) + ..setResponse(true); + + final bool result = await canLaunchUrlString(urlString); + + expect(result, isTrue); + }); + + test('handles returning false', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setCanLaunchExpectations(urlString) + ..setResponse(false); + + final bool result = await canLaunchUrlString(urlString); + + expect(result, isFalse); + }); + }); + + group('launchUrlString', () { + test('default behavior with web URL', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setLaunchExpectations( + url: urlString, + useSafariVC: true, + useWebView: true, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrlString(urlString), isTrue); + }); + + test('default behavior with non-web URL', () async { + const String urlString = 'customscheme:foo'; + mock + ..setLaunchExpectations( + url: urlString, + useSafariVC: false, + useWebView: false, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrlString(urlString), isTrue); + }); + + test('explicit default launch mode with web URL', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setLaunchExpectations( + url: urlString, + useSafariVC: true, + useWebView: true, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrlString(urlString, mode: LaunchMode.platformDefault), + isTrue); + }); + + test('explicit default launch mode with non-web URL', () async { + const String urlString = 'customscheme:foo'; + mock + ..setLaunchExpectations( + url: urlString, + useSafariVC: false, + useWebView: false, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrlString(urlString, mode: LaunchMode.platformDefault), + isTrue); + }); + + test('in-app webview', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setLaunchExpectations( + url: urlString, + useSafariVC: true, + useWebView: true, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrlString(urlString, mode: LaunchMode.inAppWebView), + isTrue); + }); + + test('external browser', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setLaunchExpectations( + url: urlString, + useSafariVC: false, + useWebView: false, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrlString(urlString, + mode: LaunchMode.externalApplication), + isTrue); + }); + + test('external non-browser only', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setLaunchExpectations( + url: urlString, + useSafariVC: false, + useWebView: false, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: true, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrlString(urlString, + mode: LaunchMode.externalNonBrowserApplication), + isTrue); + }); + + test('in-app webview without javascript', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setLaunchExpectations( + url: urlString, + useSafariVC: true, + useWebView: true, + enableJavaScript: false, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrlString(urlString, + mode: LaunchMode.inAppWebView, + webViewConfiguration: + const WebViewConfiguration(enableJavaScript: false)), + isTrue); + }); + + test('in-app webview without DOM storage', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setLaunchExpectations( + url: urlString, + useSafariVC: true, + useWebView: true, + enableJavaScript: true, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrlString(urlString, + mode: LaunchMode.inAppWebView, + webViewConfiguration: + const WebViewConfiguration(enableDomStorage: false)), + isTrue); + }); + + test('in-app webview with headers', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setLaunchExpectations( + url: urlString, + useSafariVC: true, + useWebView: true, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {'key': 'value'}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrlString(urlString, + mode: LaunchMode.inAppWebView, + webViewConfiguration: const WebViewConfiguration( + headers: {'key': 'value'})), + isTrue); + }); + + test('cannot launch a non-web URL in a webview', () async { + expect( + () async => await launchUrlString('tel:555-555-5555', + mode: LaunchMode.inAppWebView), + throwsA(isA())); + }); + + test('non-web URL with default options', () async { + const String emailLaunchUrlString = + 'mailto:smith@example.com?subject=Hello'; + mock + ..setLaunchExpectations( + url: emailLaunchUrlString, + useSafariVC: false, + useWebView: false, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrlString(emailLaunchUrlString), isTrue); + }); + + test('allows non-parseable url', () async { + // Not a valid Dart [Uri], but a valid URL on at least some platforms. + const String urlString = + 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1'; + mock + ..setLaunchExpectations( + url: urlString, + useSafariVC: false, + useWebView: false, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrlString(urlString), isTrue); + }); + }); +} diff --git a/packages/url_launcher/url_launcher/test/src/url_launcher_uri_test.dart b/packages/url_launcher/url_launcher/test/src/url_launcher_uri_test.dart new file mode 100644 index 000000000000..8286e0c43d20 --- /dev/null +++ b/packages/url_launcher/url_launcher/test/src/url_launcher_uri_test.dart @@ -0,0 +1,262 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:url_launcher/src/types.dart'; +import 'package:url_launcher/src/url_launcher_uri.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +import '../mocks/mock_url_launcher_platform.dart'; + +void main() { + final MockUrlLauncher mock = MockUrlLauncher(); + UrlLauncherPlatform.instance = mock; + + test('closeInAppWebView', () async { + await closeInAppWebView(); + expect(mock.closeWebViewCalled, isTrue); + }); + + group('canLaunchUrl', () { + test('handles returning true', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setCanLaunchExpectations(url.toString()) + ..setResponse(true); + + final bool result = await canLaunchUrl(url); + + expect(result, isTrue); + }); + + test('handles returning false', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setCanLaunchExpectations(url.toString()) + ..setResponse(false); + + final bool result = await canLaunchUrl(url); + + expect(result, isFalse); + }); + }); + + group('launchUrl', () { + test('default behavior with web URL', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setLaunchExpectations( + url: url.toString(), + useSafariVC: true, + useWebView: true, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrl(url), isTrue); + }); + + test('default behavior with non-web URL', () async { + final Uri url = Uri.parse('customscheme:foo'); + mock + ..setLaunchExpectations( + url: url.toString(), + useSafariVC: false, + useWebView: false, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrl(url), isTrue); + }); + + test('explicit default launch mode with web URL', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setLaunchExpectations( + url: url.toString(), + useSafariVC: true, + useWebView: true, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrl(url, mode: LaunchMode.platformDefault), isTrue); + }); + + test('explicit default launch mode with non-web URL', () async { + final Uri url = Uri.parse('customscheme:foo'); + mock + ..setLaunchExpectations( + url: url.toString(), + useSafariVC: false, + useWebView: false, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrl(url, mode: LaunchMode.platformDefault), isTrue); + }); + + test('in-app webview', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setLaunchExpectations( + url: url.toString(), + useSafariVC: true, + useWebView: true, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrl(url, mode: LaunchMode.inAppWebView), isTrue); + }); + + test('external browser', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setLaunchExpectations( + url: url.toString(), + useSafariVC: false, + useWebView: false, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrl(url, mode: LaunchMode.externalApplication), isTrue); + }); + + test('external non-browser only', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setLaunchExpectations( + url: url.toString(), + useSafariVC: false, + useWebView: false, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: true, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrl(url, mode: LaunchMode.externalNonBrowserApplication), + isTrue); + }); + + test('in-app webview without javascript', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setLaunchExpectations( + url: url.toString(), + useSafariVC: true, + useWebView: true, + enableJavaScript: false, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrl(url, + mode: LaunchMode.inAppWebView, + webViewConfiguration: + const WebViewConfiguration(enableJavaScript: false)), + isTrue); + }); + + test('in-app webview without DOM storage', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setLaunchExpectations( + url: url.toString(), + useSafariVC: true, + useWebView: true, + enableJavaScript: true, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrl(url, + mode: LaunchMode.inAppWebView, + webViewConfiguration: + const WebViewConfiguration(enableDomStorage: false)), + isTrue); + }); + + test('in-app webview with headers', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setLaunchExpectations( + url: url.toString(), + useSafariVC: true, + useWebView: true, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {'key': 'value'}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrl(url, + mode: LaunchMode.inAppWebView, + webViewConfiguration: const WebViewConfiguration( + headers: {'key': 'value'})), + isTrue); + }); + + test('cannot launch a non-web URL in a webview', () async { + expect( + () async => await launchUrl(Uri(scheme: 'tel', path: '555-555-5555'), + mode: LaunchMode.inAppWebView), + throwsA(isA())); + }); + + test('non-web URL with default options', () async { + final Uri emailLaunchUrl = Uri( + scheme: 'mailto', + path: 'smith@example.com', + queryParameters: {'subject': 'Hello'}, + ); + mock + ..setLaunchExpectations( + url: emailLaunchUrl.toString(), + useSafariVC: false, + useWebView: false, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrl(emailLaunchUrl), isTrue); + }); + }); +} diff --git a/packages/url_launcher/url_launcher_android/CHANGELOG.md b/packages/url_launcher/url_launcher_android/CHANGELOG.md index 9ec1f65911c6..69b96156d849 100644 --- a/packages/url_launcher/url_launcher_android/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_android/CHANGELOG.md @@ -1,3 +1,8 @@ +## 6.0.16 + +* Adds fallback querying for `canLaunch` with web URLs, to avoid false negatives + when there is a custom scheme handler. + ## 6.0.15 * Switches to an in-package method channel implementation. diff --git a/packages/url_launcher/url_launcher_android/example/lib/main.dart b/packages/url_launcher/url_launcher_android/example/lib/main.dart index 8721c587075e..7abc73430e8b 100644 --- a/packages/url_launcher/url_launcher_android/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_android/example/lib/main.dart @@ -35,73 +35,74 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { + final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; + bool _hasCallSupport = false; Future? _launched; String _phone = ''; + @override + void initState() { + super.initState(); + // Check for phone call support. + launcher.canLaunch('tel:123').then((bool result) { + setState(() { + _hasCallSupport = result; + }); + }); + } + Future _launchInBrowser(String url) async { - final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; - if (await launcher.canLaunch(url)) { - await launcher.launch( - url, - useSafariVC: false, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: {'my_header_key': 'my_header_value'}, - ); - } else { + if (!await launcher.launch( + url, + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + )) { throw 'Could not launch $url'; } } - Future _launchInWebViewOrVC(String url) async { - final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; - if (await launcher.canLaunch(url)) { - await launcher.launch( - url, - useSafariVC: true, - useWebView: true, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: {'my_header_key': 'my_header_value'}, - ); - } else { + Future _launchInWebView(String url) async { + if (!await launcher.launch( + url, + useSafariVC: true, + useWebView: true, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {'my_header_key': 'my_header_value'}, + )) { throw 'Could not launch $url'; } } Future _launchInWebViewWithJavaScript(String url) async { - final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; - if (await launcher.canLaunch(url)) { - await launcher.launch( - url, - useSafariVC: true, - useWebView: true, - enableJavaScript: true, - enableDomStorage: false, - universalLinksOnly: false, - headers: {}, - ); - } else { + if (!await launcher.launch( + url, + useSafariVC: true, + useWebView: true, + enableJavaScript: true, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + )) { throw 'Could not launch $url'; } } Future _launchInWebViewWithDomStorage(String url) async { - final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; - if (await launcher.canLaunch(url)) { - await launcher.launch( - url, - useSafariVC: true, - useWebView: true, - enableJavaScript: false, - enableDomStorage: true, - universalLinksOnly: false, - headers: {}, - ); - } else { + if (!await launcher.launch( + url, + useSafariVC: true, + useWebView: true, + enableJavaScript: false, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + )) { throw 'Could not launch $url'; } } @@ -114,25 +115,30 @@ class _MyHomePageState extends State { } } - Future _makePhoneCall(String url) async { - final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; - if (await launcher.canLaunch(url)) { - await launcher.launch( - url, - useSafariVC: false, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: true, - headers: {}, - ); - } else { - throw 'Could not launch $url'; - } + Future _makePhoneCall(String phoneNumber) async { + // Use `Uri` to ensure that `phoneNumber` is properly URL-encoded. + // Just using 'tel:$phoneNumber' would create invalid URLs in some cases, + // such as spaces in the input, which would cause `launch` to fail on some + // platforms. + final Uri launchUri = Uri( + scheme: 'tel', + path: phoneNumber, + ); + await launcher.launch( + launchUri.toString(), + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: true, + headers: {}, + ); } @override Widget build(BuildContext context) { + // onPressed calls using this URL are not gated on a 'canLaunch' check + // because the assumption is that every device can launch a web URL. const String toLaunch = 'https://www.cylog.org/headers/'; return Scaffold( appBar: AppBar( @@ -151,10 +157,14 @@ class _MyHomePageState extends State { hintText: 'Input the phone number to launch')), ), ElevatedButton( - onPressed: () => setState(() { - _launched = _makePhoneCall('tel:$_phone'); - }), - child: const Text('Make phone call'), + onPressed: _hasCallSupport + ? () => setState(() { + _launched = _makePhoneCall(_phone); + }) + : null, + child: _hasCallSupport + ? const Text('Make phone call') + : const Text('Calling not supported'), ), const Padding( padding: EdgeInsets.all(16.0), @@ -169,7 +179,7 @@ class _MyHomePageState extends State { const Padding(padding: EdgeInsets.all(16.0)), ElevatedButton( onPressed: () => setState(() { - _launched = _launchInWebViewOrVC(toLaunch); + _launched = _launchInWebView(toLaunch); }), child: const Text('Launch in app'), ), @@ -177,21 +187,21 @@ class _MyHomePageState extends State { onPressed: () => setState(() { _launched = _launchInWebViewWithJavaScript(toLaunch); }), - child: const Text('Launch in app(JavaScript ON)'), + child: const Text('Launch in app (JavaScript ON)'), ), ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewWithDomStorage(toLaunch); }), - child: const Text('Launch in app(DOM storage ON)'), + child: const Text('Launch in app (DOM storage ON)'), ), const Padding(padding: EdgeInsets.all(16.0)), ElevatedButton( onPressed: () => setState(() { - _launched = _launchInWebViewOrVC(toLaunch); + _launched = _launchInWebView(toLaunch); Timer(const Duration(seconds: 5), () { print('Closing WebView after 5 seconds...'); - UrlLauncherPlatform.instance.closeWebView(); + launcher.closeWebView(); }); }), child: const Text('Launch in app + close after 5 seconds'), diff --git a/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart b/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart index 52c46356489d..1aa093a36451 100644 --- a/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart +++ b/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart @@ -22,7 +22,24 @@ class UrlLauncherAndroid extends UrlLauncherPlatform { final LinkDelegate? linkDelegate = null; @override - Future canLaunch(String url) { + Future canLaunch(String url) async { + final bool canLaunchSpecificUrl = await _canLaunchUrl(url); + if (!canLaunchSpecificUrl) { + final String scheme = _getUrlScheme(url); + // canLaunch can return false when a custom application is registered to + // handle a web URL, but the caller doesn't have permission to see what + // that handler is. If that happens, try a web URL (with the same scheme + // variant, to be safe) that should not have a custom handler. If that + // returns true, then there is a browser, which means that there is + // at least one handler for the original URL. + if (scheme == 'http' || scheme == 'https') { + return await _canLaunchUrl('$scheme://flutter.dev'); + } + } + return canLaunchSpecificUrl; + } + + Future _canLaunchUrl(String url) { return _channel.invokeMethod( 'canLaunch', {'url': url}, @@ -57,4 +74,16 @@ class UrlLauncherAndroid extends UrlLauncherPlatform { }, ).then((bool? value) => value ?? false); } + + // Returns the part of [url] up to the first ':', or an empty string if there + // is no ':'. This deliberately does not use [Uri] to extract the scheme + // so that it works on strings that aren't actually valid URLs, since Android + // is very lenient about what it accepts for launching. + String _getUrlScheme(String url) { + final int schemeEnd = url.indexOf(':'); + if (schemeEnd == -1) { + return ''; + } + return url.substring(0, schemeEnd); + } } diff --git a/packages/url_launcher/url_launcher_android/pubspec.yaml b/packages/url_launcher/url_launcher_android/pubspec.yaml index b8706aeb13f2..3230dfeffd2e 100644 --- a/packages/url_launcher/url_launcher_android/pubspec.yaml +++ b/packages/url_launcher/url_launcher_android/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_android description: Android implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.0.15 +version: 6.0.16 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart b/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart index 909d2c100ecf..eebd8cd4c059 100644 --- a/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart +++ b/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart @@ -10,10 +10,12 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('$UrlLauncherAndroid', () { - const MethodChannel channel = - MethodChannel('plugins.flutter.io/url_launcher_android'); - final List log = []; + const MethodChannel channel = + MethodChannel('plugins.flutter.io/url_launcher_android'); + late List log; + + setUp(() { + log = []; channel.setMockMethodCallHandler((MethodCall methodCall) async { log.add(methodCall); @@ -21,19 +23,21 @@ void main() { // returned by the method channel if no return statement is specified. return null; }); + }); - tearDown(() { - log.clear(); - }); - - test('registers instance', () { - UrlLauncherAndroid.registerWith(); - expect(UrlLauncherPlatform.instance, isA()); - }); + test('registers instance', () { + UrlLauncherAndroid.registerWith(); + expect(UrlLauncherPlatform.instance, isA()); + }); - test('canLaunch', () async { + group('canLaunch', () { + test('calls through', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return true; + }); final UrlLauncherAndroid launcher = UrlLauncherAndroid(); - await launcher.canLaunch('http://example.com/'); + final bool canLaunch = await launcher.canLaunch('http://example.com/'); expect( log, [ @@ -42,16 +46,64 @@ void main() { }) ], ); + expect(canLaunch, true); }); - test('canLaunch should return false if platform returns null', () async { + test('returns false if platform returns null', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); final bool canLaunch = await launcher.canLaunch('http://example.com/'); expect(canLaunch, false); }); - test('launch', () async { + test('checks a generic URL if an http URL returns false', () async { + const String specificUrl = 'http://example.com/'; + const String genericUrl = 'http://flutter.dev'; + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return methodCall.arguments['url'] != specificUrl; + }); + + final UrlLauncherAndroid launcher = UrlLauncherAndroid(); + final bool canLaunch = await launcher.canLaunch(specificUrl); + + expect(canLaunch, true); + expect(log.length, 2); + expect(log[1].arguments['url'], genericUrl); + }); + + test('checks a generic URL if an https URL returns false', () async { + const String specificUrl = 'https://example.com/'; + const String genericUrl = 'https://flutter.dev'; + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return methodCall.arguments['url'] != specificUrl; + }); + + final UrlLauncherAndroid launcher = UrlLauncherAndroid(); + final bool canLaunch = await launcher.canLaunch(specificUrl); + + expect(canLaunch, true); + expect(log.length, 2); + expect(log[1].arguments['url'], genericUrl); + }); + + test('does not a generic URL if a non-web URL returns false', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return false; + }); + + final UrlLauncherAndroid launcher = UrlLauncherAndroid(); + final bool canLaunch = await launcher.canLaunch('sms:12345'); + + expect(canLaunch, false); + expect(log.length, 1); + }); + }); + + group('launch', () { + test('calls through', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); await launcher.launch( 'http://example.com/', @@ -77,7 +129,7 @@ void main() { ); }); - test('launch with headers', () async { + test('passes headers', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); await launcher.launch( 'http://example.com/', @@ -103,7 +155,7 @@ void main() { ); }); - test('launch universal links only', () async { + test('handles universal links only', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); await launcher.launch( 'http://example.com/', @@ -129,7 +181,7 @@ void main() { ); }); - test('launch force WebView', () async { + test('handles force WebView', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); await launcher.launch( 'http://example.com/', @@ -155,7 +207,7 @@ void main() { ); }); - test('launch force WebView enable javascript', () async { + test('handles force WebView with javascript', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); await launcher.launch( 'http://example.com/', @@ -181,7 +233,7 @@ void main() { ); }); - test('launch force WebView enable DOM storage', () async { + test('handles force WebView with DOM storage', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); await launcher.launch( 'http://example.com/', @@ -207,7 +259,7 @@ void main() { ); }); - test('launch should return false if platform returns null', () async { + test('returns false if platform returns null', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); final bool launched = await launcher.launch( 'http://example.com/', @@ -221,8 +273,10 @@ void main() { expect(launched, false); }); + }); - test('closeWebView default behavior', () async { + group('closeWebView', () { + test('calls through', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); await launcher.closeWebView(); expect( diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 5d56b1585a80..af01c64fd554 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,6 +1,12 @@ ## NEXT +* Removes unnecessary imports. + +## 2.4.0 + * Updates minimum Flutter version to 2.10. +* Adds OS version support information to README. +* Adds `setClosedCaptionFile` method to `VideoPlayerController`. ## 2.3.0 diff --git a/packages/video_player/video_player/README.md b/packages/video_player/video_player/README.md index 84b3ae7dfb54..91c1bdeb7773 100644 --- a/packages/video_player/video_player/README.md +++ b/packages/video_player/video_player/README.md @@ -4,6 +4,10 @@ A Flutter plugin for iOS, Android and Web for playing back video on a Widget surface. +| | Android | iOS | Web | +|-------------|---------|------|-------| +| **Support** | SDK 16+ | 9.0+ | Any\* | + ![The example app running in iOS](https://github.com/flutter/plugins/blob/master/packages/video_player/video_player/doc/demo_ipod.gif?raw=true) ## Installation @@ -31,7 +35,7 @@ Android Manifest file, located in `/android/app/src/main/AndroidMa > The Web platform does **not** suppport `dart:io`, so avoid using the `VideoPlayerController.file` constructor for the plugin. Using the constructor attempts to create a `VideoPlayerController.file` that will throw an `UnimplementedError`. -Different web browsers may have different video-playback capabilities (supported formats, autoplay...). Check [package:video_player_web](https://pub.dev/packages/video_player_web) for more web-specific information. +\* Different web browsers may have different video-playback capabilities (supported formats, autoplay...). Check [package:video_player_web](https://pub.dev/packages/video_player_web) for more web-specific information. The `VideoPlayerOptions.mixWithOthers` option can't be implemented in web, at least at the moment. If you use this option in web it will be silently ignored. @@ -119,7 +123,7 @@ This is not complete as of now. You can contribute to this section by [opening a You can set the playback speed on your `_controller` (instance of `VideoPlayerController`) by calling `_controller.setPlaybackSpeed`. `setPlaybackSpeed` takes a `double` speed value indicating -the rate of playback for your video. +the rate of playback for your video. For example, when given a value of `2.0`, your video will play at 2x the regular playback speed and so on. diff --git a/packages/video_player/video_player/example/integration_test/video_player_test.dart b/packages/video_player/video_player/example/integration_test/video_player_test.dart index 746c63fcbfd6..151eb93149ee 100644 --- a/packages/video_player/video_player/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player/example/integration_test/video_player_test.dart @@ -21,10 +21,13 @@ const String _videoAssetKey = kIsWeb ? 'assets/Butterfly-209.webm' : 'assets/Butterfly-209.mp4'; // Returns the URL to load an asset from this example app as a network source. +// +// TODO(stuartmorgan): Convert this to a local `HttpServer` that vends the +// assets directly, https://github.com/flutter/flutter/issues/95420 String getUrlForAssetAsNetworkSource(String assetKey) { return 'https://github.com/flutter/plugins/blob/' // This hash can be rolled forward to pick up newly-added assets. - 'cba393233e559c925a4daf71b06b4bb01c606762' + 'cb381ced070d356799dddf24aca38ce0579d3d7b' '/packages/video_player/video_player/example/' '$assetKey' '?raw=true'; @@ -56,7 +59,7 @@ void main() { (WidgetTester tester) async { final VideoPlayerController networkController = VideoPlayerController.network( - 'https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8', + 'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8', ); await networkController.initialize(); @@ -229,13 +232,8 @@ void main() { group('network videos', () { setUp(() { - // TODO(stuartmorgan): Remove this conditional and update the hash in - // getUrlForAssetAsNetworkSource as a follow-up, once the webm asset is - // checked in. - final String videoUrl = kIsWeb - ? 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm' - : getUrlForAssetAsNetworkSource(_videoAssetKey); - _controller = VideoPlayerController.network(videoUrl); + _controller = VideoPlayerController.network( + getUrlForAssetAsNetworkSource(_videoAssetKey)); }); testWidgets( diff --git a/packages/video_player/video_player/example/lib/main.dart b/packages/video_player/video_player/example/lib/main.dart index 5d496a9f5e7d..f5875975cea5 100644 --- a/packages/video_player/video_player/example/lib/main.dart +++ b/packages/video_player/video_player/example/lib/main.dart @@ -7,7 +7,6 @@ /// An example of using the plugin, controlling lifecycle and playback of the /// video. -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index b77c530b5831..39cd415dbb79 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -207,8 +207,11 @@ class VideoPlayerController extends ValueNotifier { /// null. The [package] argument must be non-null when the asset comes from a /// package and null otherwise. VideoPlayerController.asset(this.dataSource, - {this.package, this.closedCaptionFile, this.videoPlayerOptions}) - : dataSourceType = DataSourceType.asset, + {this.package, + Future? closedCaptionFile, + this.videoPlayerOptions}) + : _closedCaptionFileFuture = closedCaptionFile, + dataSourceType = DataSourceType.asset, formatHint = null, httpHeaders = const {}, super(VideoPlayerValue(duration: Duration.zero)); @@ -225,10 +228,11 @@ class VideoPlayerController extends ValueNotifier { VideoPlayerController.network( this.dataSource, { this.formatHint, - this.closedCaptionFile, + Future? closedCaptionFile, this.videoPlayerOptions, this.httpHeaders = const {}, - }) : dataSourceType = DataSourceType.network, + }) : _closedCaptionFileFuture = closedCaptionFile, + dataSourceType = DataSourceType.network, package = null, super(VideoPlayerValue(duration: Duration.zero)); @@ -237,8 +241,9 @@ class VideoPlayerController extends ValueNotifier { /// This will load the file from the file-URI given by: /// `'file://${file.path}'`. VideoPlayerController.file(File file, - {this.closedCaptionFile, this.videoPlayerOptions}) - : dataSource = 'file://${file.path}', + {Future? closedCaptionFile, this.videoPlayerOptions}) + : _closedCaptionFileFuture = closedCaptionFile, + dataSource = 'file://${file.path}', dataSourceType = DataSourceType.file, package = null, formatHint = null, @@ -250,9 +255,10 @@ class VideoPlayerController extends ValueNotifier { /// This will load the video from the input content-URI. /// This is supported on Android only. VideoPlayerController.contentUri(Uri contentUri, - {this.closedCaptionFile, this.videoPlayerOptions}) + {Future? closedCaptionFile, this.videoPlayerOptions}) : assert(defaultTargetPlatform == TargetPlatform.android, 'VideoPlayerController.contentUri is only supported on Android.'), + _closedCaptionFileFuture = closedCaptionFile, dataSource = contentUri.toString(), dataSourceType = DataSourceType.contentUri, package = null, @@ -283,13 +289,7 @@ class VideoPlayerController extends ValueNotifier { /// Only set for [asset] videos. The package that the asset was loaded from. final String? package; - /// Optional field to specify a file containing the closed - /// captioning. - /// - /// This future will be awaited and the file will be loaded when - /// [initialize()] is called. - final Future? closedCaptionFile; - + Future? _closedCaptionFileFuture; ClosedCaptionFile? _closedCaptionFile; Timer? _timer; bool _isDisposed = false; @@ -397,9 +397,8 @@ class VideoPlayerController extends ValueNotifier { } } - if (closedCaptionFile != null) { - _closedCaptionFile ??= await closedCaptionFile; - value = value.copyWith(caption: _getCaptionAt(value.position)); + if (_closedCaptionFileFuture != null) { + await _updateClosedCaptionWithFuture(_closedCaptionFileFuture); } void errorListener(Object obj) { @@ -634,6 +633,28 @@ class VideoPlayerController extends ValueNotifier { return Caption.none; } + /// Returns the file containing closed captions for the video, if any. + Future? get closedCaptionFile { + return _closedCaptionFileFuture; + } + + /// Sets a closed caption file. + /// + /// If [closedCaptionFile] is null, closed captions will be removed. + Future setClosedCaptionFile( + Future? closedCaptionFile, + ) async { + await _updateClosedCaptionWithFuture(closedCaptionFile); + _closedCaptionFileFuture = closedCaptionFile; + } + + Future _updateClosedCaptionWithFuture( + Future? closedCaptionFile, + ) async { + _closedCaptionFile = await closedCaptionFile; + value = value.copyWith(caption: _getCaptionAt(value.position)); + } + void _updatePosition(Duration position) { value = value.copyWith( position: position, diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 88e45c5be057..0d654a4330a7 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.3.0 +version: 2.4.0 environment: sdk: ">=2.14.0 <3.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 index 5e31e22c7010..9eca7f921acb 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -5,10 +5,8 @@ import 'dart:async'; import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:video_player/video_player.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; @@ -72,6 +70,11 @@ class FakeController extends ValueNotifier @override void setCaptionOffset(Duration delay) {} + + @override + Future setClosedCaptionFile( + Future? closedCaptionFile, + ) async {} } Future _loadClosedCaption() async => @@ -672,6 +675,37 @@ void main() { await controller.seekTo(const Duration(milliseconds: 300)); expect(controller.value.caption.text, 'one'); }); + + test('setClosedCapitonFile loads caption file', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + + await controller.initialize(); + expect(controller.closedCaptionFile, null); + + await controller.setClosedCaptionFile(_loadClosedCaption()); + expect( + (await controller.closedCaptionFile)!.captions, + (await _loadClosedCaption()).captions, + ); + }); + + test('setClosedCapitonFile removes/changes caption file', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + closedCaptionFile: _loadClosedCaption(), + ); + + await controller.initialize(); + expect( + (await controller.closedCaptionFile)!.captions, + (await _loadClosedCaption()).captions, + ); + + await controller.setClosedCaptionFile(null); + expect(controller.closedCaptionFile, null); + }); }); group('Platform callbacks', () { diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index 774589c05f84..16dd52ca6da0 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Removes unnecessary imports. + +## 2.3.2 + +* Updates ExoPlayer to 2.17.0. + ## 2.3.1 * Renames internal method channels to avoid potential confusion with the diff --git a/packages/video_player/video_player_android/android/build.gradle b/packages/video_player/video_player_android/android/build.gradle index 6e6e8c792150..e565a9364bd8 100644 --- a/packages/video_player/video_player_android/android/build.gradle +++ b/packages/video_player/video_player_android/android/build.gradle @@ -43,10 +43,10 @@ android { } dependencies { - implementation 'com.google.android.exoplayer:exoplayer-core:2.14.1' - implementation 'com.google.android.exoplayer:exoplayer-hls:2.14.1' - implementation 'com.google.android.exoplayer:exoplayer-dash:2.14.1' - implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.14.1' + implementation 'com.google.android.exoplayer:exoplayer-core:2.17.0' + implementation 'com.google.android.exoplayer:exoplayer-hls:2.17.0' + implementation 'com.google.android.exoplayer:exoplayer-dash:2.17.0' + implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.17.0' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-inline:3.9.0' } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 33593267338c..dc7c88144583 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -12,13 +12,13 @@ import android.view.Surface; import androidx.annotation.NonNull; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.Listener; -import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; @@ -28,7 +28,7 @@ import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.util.Util; import io.flutter.plugin.common.EventChannel; @@ -45,7 +45,7 @@ final class VideoPlayer { private static final String FORMAT_HLS = "hls"; private static final String FORMAT_OTHER = "other"; - private SimpleExoPlayer exoPlayer; + private ExoPlayer exoPlayer; private Surface surface; @@ -71,10 +71,9 @@ final class VideoPlayer { this.textureEntry = textureEntry; this.options = options; - exoPlayer = new SimpleExoPlayer.Builder(context).build(); + exoPlayer = new ExoPlayer.Builder(context).build(); Uri uri = Uri.parse(dataSource); - DataSource.Factory dataSourceFactory; if (isHTTP(uri)) { DefaultHttpDataSource.Factory httpDataSourceFactory = @@ -87,7 +86,7 @@ final class VideoPlayer { } dataSourceFactory = httpDataSourceFactory; } else { - dataSourceFactory = new DefaultDataSourceFactory(context, "ExoPlayer"); + dataSourceFactory = new DefaultDataSource.Factory(context); } MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context); @@ -107,6 +106,7 @@ private static boolean isHTTP(Uri uri) { private MediaSource buildMediaSource( Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) { + int type; if (formatHint == null) { type = Util.inferContentType(uri.getLastPathSegment()); @@ -133,12 +133,12 @@ private MediaSource buildMediaSource( case C.TYPE_SS: return new SsMediaSource.Factory( new DefaultSsChunkSource.Factory(mediaDataSourceFactory), - new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) + new DefaultDataSource.Factory(context, mediaDataSourceFactory)) .createMediaSource(MediaItem.fromUri(uri)); case C.TYPE_DASH: return new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), - new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) + new DefaultDataSource.Factory(context, mediaDataSourceFactory)) .createMediaSource(MediaItem.fromUri(uri)); case C.TYPE_HLS: return new HlsMediaSource.Factory(mediaDataSourceFactory) @@ -207,7 +207,7 @@ public void onPlaybackStateChanged(final int playbackState) { } @Override - public void onPlayerError(final ExoPlaybackException error) { + public void onPlayerError(final PlaybackException error) { setBuffering(false); if (eventSink != null) { eventSink.error("VideoError", "Video player had error " + error, null); @@ -225,8 +225,7 @@ void sendBufferingUpdate() { eventSink.success(event); } - @SuppressWarnings("deprecation") - private static void setAudioAttributes(SimpleExoPlayer exoPlayer, boolean isMixMode) { + private static void setAudioAttributes(ExoPlayer exoPlayer, boolean isMixMode) { exoPlayer.setAudioAttributes( new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build(), !isMixMode); } diff --git a/packages/video_player/video_player_android/example/integration_test/video_player_test.dart b/packages/video_player/video_player_android/example/integration_test/video_player_test.dart index b80ed745a6f9..77a618bbbdfc 100644 --- a/packages/video_player/video_player_android/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player_android/example/integration_test/video_player_test.dart @@ -24,10 +24,13 @@ const Duration _playDuration = Duration(seconds: 1); const String _videoAssetKey = 'assets/Butterfly-209.mp4'; // Returns the URL to load an asset from this example app as a network source. +// +// TODO(stuartmorgan): Convert this to a local `HttpServer` that vends the +// assets directly, https://github.com/flutter/flutter/issues/95420 String getUrlForAssetAsNetworkSource(String assetKey) { return 'https://github.com/flutter/plugins/blob/' // This hash can be rolled forward to pick up newly-added assets. - 'cba393233e559c925a4daf71b06b4bb01c606762' + 'cb381ced070d356799dddf24aca38ce0579d3d7b' '/packages/video_player/video_player/example/' '$assetKey' '?raw=true'; @@ -155,7 +158,7 @@ void main() { testWidgets('live stream duration != 0', (WidgetTester tester) async { final MiniController livestreamController = MiniController.network( - 'https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8', + 'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8', ); await livestreamController.initialize(); diff --git a/packages/video_player/video_player_android/example/lib/main.dart b/packages/video_player/video_player_android/example/lib/main.dart index cab6eb802ca5..bca4e291efff 100644 --- a/packages/video_player/video_player_android/example/lib/main.dart +++ b/packages/video_player/video_player_android/example/lib/main.dart @@ -4,7 +4,6 @@ // ignore_for_file: public_member_api_docs -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'mini_controller.dart'; diff --git a/packages/video_player/video_player_android/example/lib/mini_controller.dart b/packages/video_player/video_player_android/example/lib/mini_controller.dart index 9bb8e90b65ae..498dbffc9e84 100644 --- a/packages/video_player/video_player_android/example/lib/mini_controller.dart +++ b/packages/video_player/video_player_android/example/lib/mini_controller.dart @@ -8,7 +8,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart index 31d0744e51dc..5c5fd809c199 100644 --- a/packages/video_player/video_player_android/lib/src/android_video_player.dart +++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index 15c063f83d40..aa288ed71eac 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_android description: Android implementation of the video_player plugin. repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.3.1 +version: 2.3.2 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index c36c9878d1a1..d77c36c915b6 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,16 @@ +## NEXT + +* Removes unnecessary imports. + +## 2.3.3 + +* Fix XCUITest based on the new voice over announcement for tooltips. + See: https://github.com/flutter/flutter/pull/87684 + +## 2.3.2 + +* Applies the standardized transform for videos with different orientations. + ## 2.3.1 * Renames internal method channels to avoid potential confusion with the diff --git a/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart b/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart index a457d9226e3e..528723d092b4 100644 --- a/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart @@ -24,10 +24,13 @@ const Duration _playDuration = Duration(seconds: 1); const String _videoAssetKey = 'assets/Butterfly-209.mp4'; // Returns the URL to load an asset from this example app as a network source. +// +// TODO(stuartmorgan): Convert this to a local `HttpServer` that vends the +// assets directly, https://github.com/flutter/flutter/issues/95420 String getUrlForAssetAsNetworkSource(String assetKey) { return 'https://github.com/flutter/plugins/blob/' // This hash can be rolled forward to pick up newly-added assets. - 'cba393233e559c925a4daf71b06b4bb01c606762' + 'cb381ced070d356799dddf24aca38ce0579d3d7b' '/packages/video_player/video_player/example/' '$assetKey' '?raw=true'; @@ -167,7 +170,7 @@ void main() { testWidgets('live stream duration != 0', (WidgetTester tester) async { final MiniController livestreamController = MiniController.network( - 'https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8', + 'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8', ); await livestreamController.initialize(); diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m index cbf2866aa071..7decd04bd168 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m @@ -7,6 +7,7 @@ @import XCTest; #import +#import @interface FLTVideoPlayer : NSObject @property(readonly, nonatomic) AVPlayer *player; @@ -17,6 +18,44 @@ @interface FLTVideoPlayerPlugin (Test) NSMutableDictionary *playersByTextureId; @end +@interface FakeAVAssetTrack : AVAssetTrack +@property(readonly, nonatomic) CGAffineTransform preferredTransform; +@property(readonly, nonatomic) CGSize naturalSize; +@property(readonly, nonatomic) UIImageOrientation orientation; +- (instancetype)initWithOrientation:(UIImageOrientation)orientation; +@end + +@implementation FakeAVAssetTrack + +- (instancetype)initWithOrientation:(UIImageOrientation)orientation { + _orientation = orientation; + _naturalSize = CGSizeMake(800, 600); + return self; +} + +- (CGAffineTransform)preferredTransform { + switch (_orientation) { + case UIImageOrientationUp: + return CGAffineTransformMake(1, 0, 0, 1, 0, 0); + case UIImageOrientationDown: + return CGAffineTransformMake(-1, 0, 0, -1, 0, 0); + case UIImageOrientationLeft: + return CGAffineTransformMake(0, -1, 1, 0, 0, 0); + case UIImageOrientationRight: + return CGAffineTransformMake(0, 1, -1, 0, 0, 0); + case UIImageOrientationUpMirrored: + return CGAffineTransformMake(-1, 0, 0, 1, 0, 0); + case UIImageOrientationDownMirrored: + return CGAffineTransformMake(1, 0, 0, -1, 0, 0); + case UIImageOrientationLeftMirrored: + return CGAffineTransformMake(0, -1, -1, 0, 0, 0); + case UIImageOrientationRightMirrored: + return CGAffineTransformMake(0, 1, 1, 0, 0, 0); + } +} + +@end + @interface VideoPlayerTests : XCTestCase @end @@ -121,6 +160,17 @@ - (void)testHLSControls { XCTAssertEqualWithAccuracy([videoInitialization[@"duration"] intValue], 4000, 200); } +- (void)testTransformFix { + [self validateTransformFixForOrientation:UIImageOrientationUp]; + [self validateTransformFixForOrientation:UIImageOrientationDown]; + [self validateTransformFixForOrientation:UIImageOrientationLeft]; + [self validateTransformFixForOrientation:UIImageOrientationRight]; + [self validateTransformFixForOrientation:UIImageOrientationUpMirrored]; + [self validateTransformFixForOrientation:UIImageOrientationDownMirrored]; + [self validateTransformFixForOrientation:UIImageOrientationLeftMirrored]; + [self validateTransformFixForOrientation:UIImageOrientationRightMirrored]; +} + - (NSDictionary *)testPlugin:(FLTVideoPlayerPlugin *)videoPlayerPlugin uri:(NSString *)uri { FlutterError *error; @@ -175,4 +225,47 @@ - (void)testHLSControls { return initializationEvent; } +- (void)validateTransformFixForOrientation:(UIImageOrientation)orientation { + AVAssetTrack *track = [[FakeAVAssetTrack alloc] initWithOrientation:orientation]; + CGAffineTransform t = FLTGetStandardizedTransformForTrack(track); + CGSize size = track.naturalSize; + CGFloat expectX, expectY; + switch (orientation) { + case UIImageOrientationUp: + expectX = 0; + expectY = 0; + break; + case UIImageOrientationDown: + expectX = size.width; + expectY = size.height; + break; + case UIImageOrientationLeft: + expectX = 0; + expectY = size.width; + break; + case UIImageOrientationRight: + expectX = size.height; + expectY = 0; + break; + case UIImageOrientationUpMirrored: + expectX = size.width; + expectY = 0; + break; + case UIImageOrientationDownMirrored: + expectX = 0; + expectY = size.height; + break; + case UIImageOrientationLeftMirrored: + expectX = size.height; + expectY = size.width; + break; + case UIImageOrientationRightMirrored: + expectX = 0; + expectY = 0; + break; + } + XCTAssertEqual(t.tx, expectX); + XCTAssertEqual(t.ty, expectY); +} + @end diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m index 2933cf36feae..b9f0f16bb27b 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m @@ -30,16 +30,20 @@ - (void)testPlayVideo { XCTAssertTrue([playButton waitForExistenceWithTimeout:30.0]); [playButton tap]; - XCUIElement *playbackSpeed1x = app.staticTexts[@"Playback speed\n1.0x"]; - XCTAssertTrue([playbackSpeed1x waitForExistenceWithTimeout:30.0]); + NSPredicate *find1xButton = [NSPredicate predicateWithFormat:@"label CONTAINS '1.0x'"]; + XCUIElement *playbackSpeed1x = [app.staticTexts elementMatchingPredicate:find1xButton]; + BOOL foundPlaybackSpeed1x = [playbackSpeed1x waitForExistenceWithTimeout:30.0]; + XCTAssertTrue(foundPlaybackSpeed1x); [playbackSpeed1x tap]; XCUIElement *playbackSpeed5xButton = app.buttons[@"5.0x"]; XCTAssertTrue([playbackSpeed5xButton waitForExistenceWithTimeout:30.0]); [playbackSpeed5xButton tap]; - XCUIElement *playbackSpeed5x = app.staticTexts[@"Playback speed\n5.0x"]; - XCTAssertTrue([playbackSpeed5x waitForExistenceWithTimeout:30.0]); + NSPredicate *find5xButton = [NSPredicate predicateWithFormat:@"label CONTAINS '5.0x'"]; + XCUIElement *playbackSpeed5x = [app.staticTexts elementMatchingPredicate:find5xButton]; + BOOL foundPlaybackSpeed5x = [playbackSpeed5x waitForExistenceWithTimeout:30.0]; + XCTAssertTrue(foundPlaybackSpeed5x); // Cycle through tabs. for (NSString *tabName in @[ @"Asset", @"Remote" ]) { diff --git a/packages/video_player/video_player_avfoundation/example/lib/main.dart b/packages/video_player/video_player_avfoundation/example/lib/main.dart index cab6eb802ca5..bca4e291efff 100644 --- a/packages/video_player/video_player_avfoundation/example/lib/main.dart +++ b/packages/video_player/video_player_avfoundation/example/lib/main.dart @@ -4,7 +4,6 @@ // ignore_for_file: public_member_api_docs -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'mini_controller.dart'; diff --git a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart index 9bb8e90b65ae..498dbffc9e84 100644 --- a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart +++ b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart @@ -8,7 +8,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/AVAssetTrackUtils.h b/packages/video_player/video_player_avfoundation/ios/Classes/AVAssetTrackUtils.h new file mode 100644 index 000000000000..9d736bc21afe --- /dev/null +++ b/packages/video_player/video_player_avfoundation/ios/Classes/AVAssetTrackUtils.h @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +/** + * Returns a standardized transform + * according to the orientation of the track. + * + * Note: https://stackoverflow.com/questions/64161544 + * `AVAssetTrack.preferredTransform` can have wrong `tx` and `ty`. + */ +CGAffineTransform FLTGetStandardizedTransformForTrack(AVAssetTrack* track); diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/AVAssetTrackUtils.m b/packages/video_player/video_player_avfoundation/ios/Classes/AVAssetTrackUtils.m new file mode 100644 index 000000000000..de75859a94a4 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/ios/Classes/AVAssetTrackUtils.m @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +CGAffineTransform FLTGetStandardizedTransformForTrack(AVAssetTrack *track) { + CGAffineTransform t = track.preferredTransform; + CGSize size = track.naturalSize; + // Each case of control flows corresponds to a specific + // `UIImageOrientation`, with 8 cases in total. + if (t.a == 1 && t.b == 0 && t.c == 0 && t.d == 1) { + // UIImageOrientationUp + t.tx = 0; + t.ty = 0; + } else if (t.a == -1 && t.b == 0 && t.c == 0 && t.d == -1) { + // UIImageOrientationDown + t.tx = size.width; + t.ty = size.height; + } else if (t.a == 0 && t.b == -1 && t.c == 1 && t.d == 0) { + // UIImageOrientationLeft + t.tx = 0; + t.ty = size.width; + } else if (t.a == 0 && t.b == 1 && t.c == -1 && t.d == 0) { + // UIImageOrientationRight + t.tx = size.height; + t.ty = 0; + } else if (t.a == -1 && t.b == 0 && t.c == 0 && t.d == 1) { + // UIImageOrientationUpMirrored + t.tx = size.width; + t.ty = 0; + } else if (t.a == 1 && t.b == 0 && t.c == 0 && t.d == -1) { + // UIImageOrientationDownMirrored + t.tx = 0; + t.ty = size.height; + } else if (t.a == 0 && t.b == -1 && t.c == -1 && t.d == 0) { + // UIImageOrientationLeftMirrored + t.tx = size.height; + t.ty = size.width; + } else if (t.a == 0 && t.b == 1 && t.c == 1 && t.d == 0) { + // UIImageOrientationRightMirrored + t.tx = 0; + t.ty = 0; + } + return t; +} diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m index 2fb9f7f55600..a95779b1cbab 100644 --- a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m +++ b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m @@ -3,8 +3,11 @@ // found in the LICENSE file. #import "FLTVideoPlayerPlugin.h" + #import #import + +#import "AVAssetTrackUtils.h" #import "messages.g.h" #if !__has_feature(objc_arc) @@ -187,29 +190,6 @@ - (instancetype)initWithURL:(NSURL *)url return [self initWithPlayerItem:item frameUpdater:frameUpdater]; } -- (CGAffineTransform)fixTransform:(AVAssetTrack *)videoTrack { - CGAffineTransform transform = videoTrack.preferredTransform; - // TODO(@recastrodiaz): why do we need to do this? Why is the preferredTransform incorrect? - // At least 2 user videos show a black screen when in portrait mode if we directly use the - // videoTrack.preferredTransform Setting tx to the height of the video instead of 0, properly - // displays the video https://github.com/flutter/flutter/issues/17606#issuecomment-413473181 - if (transform.tx == 0 && transform.ty == 0) { - NSInteger rotationDegrees = (NSInteger)round(radiansToDegrees(atan2(transform.b, transform.a))); - NSLog(@"TX and TY are 0. Rotation: %ld. Natural width,height: %f, %f", (long)rotationDegrees, - videoTrack.naturalSize.width, videoTrack.naturalSize.height); - if (rotationDegrees == 90) { - NSLog(@"Setting transform tx"); - transform.tx = videoTrack.naturalSize.height; - transform.ty = 0; - } else if (rotationDegrees == 270) { - NSLog(@"Setting transform ty"); - transform.tx = 0; - transform.ty = videoTrack.naturalSize.width; - } - } - return transform; -} - - (instancetype)initWithPlayerItem:(AVPlayerItem *)item frameUpdater:(FLTFrameUpdater *)frameUpdater { self = [super init]; @@ -226,7 +206,7 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item if ([videoTrack statusOfValueForKey:@"preferredTransform" error:nil] == AVKeyValueStatusLoaded) { // Rotate the video by using a videoComposition and the preferredTransform - self->_preferredTransform = [self fixTransform:videoTrack]; + self->_preferredTransform = FLTGetStandardizedTransformForTrack(videoTrack); // Note: // https://developer.apple.com/documentation/avfoundation/avplayeritem/1388818-videocomposition // Video composition can only be used with file-based media and is not supported for diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index 5dc6862c41df..b5ebedda41e1 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 0e42c26de829..b3cc69eca958 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_avfoundation description: iOS implementation of the video_player plugin. repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.3.1 +version: 2.3.3 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index 52612207d8f3..4304fd470ba2 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,3 +1,16 @@ +## NEXT + +* Removes unnecessary imports. + +## 5.1.2 + +* Adopts `Object.hash`. +* Removes obsolete dependency on `pedantic`. + +## 5.1.1 + +* Adds `rotationCorrection` (for Android playing videos recorded in landscapeRight [#60327](https://github.com/flutter/flutter/issues/60327)). + ## 5.1.0 * Adds `allowBackgroundPlayback` to `VideoPlayerOptions`. diff --git a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart index 2aa7fb30e5f2..be264ca25061 100644 --- a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart +++ b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -115,6 +114,7 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { duration: Duration(milliseconds: map['duration']! as int), size: Size((map['width'] as num?)?.toDouble() ?? 0.0, (map['height'] as num?)?.toDouble() ?? 0.0), + rotationCorrection: map['rotationCorrection'] as int? ?? 0, ); case 'completed': return VideoEvent( diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index 8a61005c429e..78173f1fb63c 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -199,8 +199,8 @@ class VideoEvent { /// /// The [eventType] argument is required. /// - /// Depending on the [eventType], the [duration], [size] and [buffered] - /// arguments can be null. + /// Depending on the [eventType], the [duration], [size], + /// [rotationCorrection], and [buffered] arguments can be null. // TODO(stuartmorgan): Temporarily suppress warnings about not using const // in all of the other video player packages, fix this, and then update // the other packages to use const. @@ -209,6 +209,7 @@ class VideoEvent { required this.eventType, this.duration, this.size, + this.rotationCorrection, this.buffered, }); @@ -225,6 +226,11 @@ class VideoEvent { /// Only used if [eventType] is [VideoEventType.initialized]. final Size? size; + /// Degrees to rotate the video (clockwise) so it is displayed correctly. + /// + /// Only used if [eventType] is [VideoEventType.initialized]. + final int? rotationCorrection; + /// Buffered parts of the video. /// /// Only used if [eventType] is [VideoEventType.bufferingUpdate]. @@ -238,15 +244,18 @@ class VideoEvent { eventType == other.eventType && duration == other.duration && size == other.size && + rotationCorrection == other.rotationCorrection && listEquals(buffered, other.buffered); } @override - int get hashCode => - eventType.hashCode ^ - duration.hashCode ^ - size.hashCode ^ - buffered.hashCode; + int get hashCode => Object.hash( + eventType, + duration, + size, + rotationCorrection, + buffered, + ); } /// Type of the event. @@ -335,7 +344,7 @@ class DurationRange { end == other.end; @override - int get hashCode => start.hashCode ^ end.hashCode; + int get hashCode => Object.hash(start, end); } /// [VideoPlayerOptions] can be optionally used to set additional player settings diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index b66fa0b46d75..7a18568f22b5 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/video_player/v issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 5.1.0 +version: 5.1.2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" dependencies: flutter: @@ -18,5 +18,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 pigeon: 0.1.21 diff --git a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart index 75baba45f763..ca17196cf523 100644 --- a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart +++ b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart @@ -257,6 +257,20 @@ void main() { }), (ByteData? data) {}); + await _ambiguate(ServicesBinding.instance) + ?.defaultBinaryMessenger + .handlePlatformMessage( + 'flutter.io/videoPlayer/videoEvents123', + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'initialized', + 'duration': 98765, + 'width': 1920, + 'height': 1080, + 'rotationCorrection': 180, + }), + (ByteData? data) {}); + await _ambiguate(ServicesBinding.instance) ?.defaultBinaryMessenger .handlePlatformMessage( @@ -316,6 +330,13 @@ void main() { eventType: VideoEventType.initialized, duration: const Duration(milliseconds: 98765), size: const Size(1920, 1080), + rotationCorrection: 0, + ), + VideoEvent( + eventType: VideoEventType.initialized, + duration: const Duration(milliseconds: 98765), + size: const Size(1920, 1080), + rotationCorrection: 180, ), VideoEvent(eventType: VideoEventType.completed), VideoEvent( diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 1cd428c4deea..00788c4386fe 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,15 @@ +## NEXT + +* Removes unnecessary imports. + +## 2.0.8 + +* Ensures `buffering` state is only removed when the browser reports enough data + has been buffered so that the video can likely play through without stopping + (`onCanPlayThrough`). Issue [#94630](https://github.com/flutter/flutter/issues/94630). +* Improves testability of the `_VideoPlayer` private class. +* Ensures that tests that listen to a Stream fail "fast" (1 second max timeout). + ## 2.0.7 * Internal code cleanup for stricter analysis options. diff --git a/packages/video_player/video_player_web/example/integration_test/utils.dart b/packages/video_player/video_player_web/example/integration_test/utils.dart new file mode 100644 index 000000000000..b0118514053a --- /dev/null +++ b/packages/video_player/video_player_web/example/integration_test/utils.dart @@ -0,0 +1,16 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Returns the URL to load an asset from this example app as a network source. +// +// TODO(stuartmorgan): Convert this to a local `HttpServer` that vends the +// assets directly, https://github.com/flutter/flutter/issues/95420 +String getUrlForAssetAsNetworkSource(String assetKey) { + return 'https://github.com/flutter/plugins/blob/' + // This hash can be rolled forward to pick up newly-added assets. + 'cb381ced070d356799dddf24aca38ce0579d3d7b' + '/packages/video_player/video_player/example/' + '$assetKey' + '?raw=true'; +} diff --git a/packages/video_player/video_player_web/example/integration_test/video_player_test.dart b/packages/video_player/video_player_web/example/integration_test/video_player_test.dart new file mode 100644 index 000000000000..41aba9792e23 --- /dev/null +++ b/packages/video_player/video_player_web/example/integration_test/video_player_test.dart @@ -0,0 +1,195 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html' as html; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; +import 'package:video_player_web/src/video_player.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('VideoPlayer', () { + late html.VideoElement video; + + setUp(() { + // Never set "src" on the video, so this test doesn't hit the network! + video = html.VideoElement() + ..controls = true + ..setAttribute('playsinline', 'false'); + }); + + testWidgets('fixes critical video element config', (WidgetTester _) async { + VideoPlayer(videoElement: video).initialize(); + + expect(video.controls, isFalse, + reason: 'Video is controlled through code'); + expect(video.getAttribute('autoplay'), 'false', + reason: 'Cannot autoplay on the web'); + expect(video.getAttribute('playsinline'), 'true', + reason: 'Needed by safari iOS'); + }); + + testWidgets('setVolume', (WidgetTester tester) async { + final VideoPlayer player = VideoPlayer(videoElement: video)..initialize(); + + player.setVolume(0); + + expect(video.volume, isZero, reason: 'Volume should be zero'); + expect(video.muted, isTrue, reason: 'muted attribute should be true'); + + expect(() { + player.setVolume(-0.0001); + }, throwsAssertionError, reason: 'Volume cannot be < 0'); + + expect(() { + player.setVolume(1.0001); + }, throwsAssertionError, reason: 'Volume cannot be > 1'); + }); + + testWidgets('setPlaybackSpeed', (WidgetTester tester) async { + final VideoPlayer player = VideoPlayer(videoElement: video)..initialize(); + + expect(() { + player.setPlaybackSpeed(-1); + }, throwsAssertionError, reason: 'Playback speed cannot be < 0'); + + expect(() { + player.setPlaybackSpeed(0); + }, throwsAssertionError, reason: 'Playback speed cannot be == 0'); + }); + + testWidgets('seekTo', (WidgetTester tester) async { + final VideoPlayer player = VideoPlayer(videoElement: video)..initialize(); + + expect(() { + player.seekTo(const Duration(seconds: -1)); + }, throwsAssertionError, reason: 'Cannot seek into negative numbers'); + }); + + // The events tested in this group do *not* represent the actual sequence + // of events from a real "video" element. They're crafted to test the + // behavior of the VideoPlayer in different states with different events. + group('events', () { + late StreamController streamController; + late VideoPlayer player; + late Stream timedStream; + + final Set bufferingEvents = { + VideoEventType.bufferingStart, + VideoEventType.bufferingEnd, + }; + + setUp(() { + streamController = StreamController(); + player = + VideoPlayer(videoElement: video, eventController: streamController) + ..initialize(); + + // This stream will automatically close after 100 ms without seeing any events + timedStream = streamController.stream.timeout( + const Duration(milliseconds: 100), + onTimeout: (EventSink sink) { + sink.close(); + }, + ); + }); + + testWidgets('buffering dispatches only when it changes', + (WidgetTester tester) async { + // Take all the "buffering" events that we see during the next few seconds + final Future> stream = timedStream + .where( + (VideoEvent event) => bufferingEvents.contains(event.eventType)) + .map((VideoEvent event) => + event.eventType == VideoEventType.bufferingStart) + .toList(); + + // Simulate some events coming from the player... + player.setBuffering(true); + player.setBuffering(true); + player.setBuffering(true); + player.setBuffering(false); + player.setBuffering(false); + player.setBuffering(true); + player.setBuffering(false); + player.setBuffering(true); + player.setBuffering(false); + + final List events = await stream; + + expect(events, hasLength(6)); + expect(events, [true, false, true, false, true, false]); + }); + + testWidgets('canplay event does not change buffering state', + (WidgetTester tester) async { + // Take all the "buffering" events that we see during the next few seconds + final Future> stream = timedStream + .where( + (VideoEvent event) => bufferingEvents.contains(event.eventType)) + .map((VideoEvent event) => + event.eventType == VideoEventType.bufferingStart) + .toList(); + + player.setBuffering(true); + + // Simulate "canplay" event... + video.dispatchEvent(html.Event('canplay')); + + final List events = await stream; + + expect(events, hasLength(1)); + expect(events, [true]); + }); + + testWidgets('canplaythrough event does change buffering state', + (WidgetTester tester) async { + // Take all the "buffering" events that we see during the next few seconds + final Future> stream = timedStream + .where( + (VideoEvent event) => bufferingEvents.contains(event.eventType)) + .map((VideoEvent event) => + event.eventType == VideoEventType.bufferingStart) + .toList(); + + player.setBuffering(true); + + // Simulate "canplaythrough" event... + video.dispatchEvent(html.Event('canplaythrough')); + + final List events = await stream; + + expect(events, hasLength(2)); + expect(events, [true, false]); + }); + + testWidgets('initialized dispatches only once', + (WidgetTester tester) async { + // Dispatch some bogus "canplay" events from the video object + video.dispatchEvent(html.Event('canplay')); + video.dispatchEvent(html.Event('canplay')); + video.dispatchEvent(html.Event('canplay')); + + // Take all the "initialized" events that we see during the next few seconds + final Future> stream = timedStream + .where((VideoEvent event) => + event.eventType == VideoEventType.initialized) + .toList(); + + video.dispatchEvent(html.Event('canplay')); + video.dispatchEvent(html.Event('canplay')); + video.dispatchEvent(html.Event('canplay')); + + final List events = await stream; + + expect(events, hasLength(1)); + expect(events[0].eventType, VideoEventType.initialized); + }); + }); + }); +} diff --git a/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart b/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart index 97b03642cd07..5053ea6e5b04 100644 --- a/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart +++ b/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart @@ -11,10 +11,15 @@ import 'package:integration_test/integration_test.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; import 'package:video_player_web/video_player_web.dart'; +import 'utils.dart'; + +// Use WebM to allow CI to run tests in Chromium. +const String _videoAssetKey = 'assets/Butterfly-209.webm'; + void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('VideoPlayer for Web', () { + group('VideoPlayerWeb plugin (hits network)', () { late Future textureId; setUp(() { @@ -23,8 +28,7 @@ void main() { .create( DataSource( sourceType: DataSourceType.network, - uri: - 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + uri: getUrlForAssetAsNetworkSource(_videoAssetKey), ), ) .then((int? textureId) => textureId!); @@ -38,9 +42,9 @@ void main() { expect( VideoPlayerPlatform.instance.create( DataSource( - sourceType: DataSourceType.network, - uri: - 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'), + sourceType: DataSourceType.network, + uri: getUrlForAssetAsNetworkSource(_videoAssetKey), + ), ), completion(isNonZero)); }); @@ -100,9 +104,9 @@ void main() { (WidgetTester tester) async { final int videoPlayerId = (await VideoPlayerPlatform.instance.create( DataSource( - sourceType: DataSourceType.network, - uri: - 'https://flutter.github.io/assets-for-api-docs/assets/videos/_non_existent_video.mp4'), + sourceType: DataSourceType.network, + uri: getUrlForAssetAsNetworkSource('assets/__non_existent.webm'), + ), ))!; final Stream eventStream = @@ -113,7 +117,7 @@ void main() { await VideoPlayerPlatform.instance.play(videoPlayerId); expect(() async { - await eventStream.last; + await eventStream.timeout(const Duration(seconds: 5)).last; }, throwsA(isA())); }); @@ -164,5 +168,40 @@ void main() { expect(VideoPlayerPlatform.instance.setMixWithOthers(true), completes); expect(VideoPlayerPlatform.instance.setMixWithOthers(false), completes); }); + + testWidgets('video playback lifecycle', (WidgetTester tester) async { + final int videoPlayerId = await textureId; + final Stream eventStream = + VideoPlayerPlatform.instance.videoEventsFor(videoPlayerId); + + final Future> stream = eventStream.timeout( + const Duration(seconds: 1), + onTimeout: (EventSink sink) { + sink.close(); + }, + ).toList(); + + await VideoPlayerPlatform.instance.setVolume(videoPlayerId, 0); + await VideoPlayerPlatform.instance.play(videoPlayerId); + + // Let the video play, until we stop seeing events for a second + final List events = await stream; + + await VideoPlayerPlatform.instance.pause(videoPlayerId); + + // The expected list of event types should look like this: + // 1. bufferingStart, + // 2. bufferingUpdate (videoElement.onWaiting), + // 3. initialized (videoElement.onCanPlay), + // 4. bufferingEnd (videoElement.onCanPlayThrough), + expect( + events.map((VideoEvent e) => e.eventType), + equals([ + VideoEventType.bufferingStart, + VideoEventType.bufferingUpdate, + VideoEventType.initialized, + VideoEventType.bufferingEnd + ])); + }); }); } diff --git a/packages/video_player/video_player_web/lib/src/video_player.dart b/packages/video_player/video_player_web/lib/src/video_player.dart new file mode 100644 index 000000000000..45d90d675b83 --- /dev/null +++ b/packages/video_player/video_player_web/lib/src/video_player.dart @@ -0,0 +1,253 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html' as html; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; + +// An error code value to error name Map. +// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code +const Map _kErrorValueToErrorName = { + 1: 'MEDIA_ERR_ABORTED', + 2: 'MEDIA_ERR_NETWORK', + 3: 'MEDIA_ERR_DECODE', + 4: 'MEDIA_ERR_SRC_NOT_SUPPORTED', +}; + +// An error code value to description Map. +// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code +const Map _kErrorValueToErrorDescription = { + 1: 'The user canceled the fetching of the video.', + 2: 'A network error occurred while fetching the video, despite having previously been available.', + 3: 'An error occurred while trying to decode the video, despite having previously been determined to be usable.', + 4: 'The video has been found to be unsuitable (missing or in a format not supported by your browser).', +}; + +// The default error message, when the error is an empty string +// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/message +const String _kDefaultErrorMessage = + 'No further diagnostic information can be determined or provided.'; + +/// Wraps a [html.VideoElement] so its API complies with what is expected by the plugin. +class VideoPlayer { + /// Create a [VideoPlayer] from a [html.VideoElement] instance. + VideoPlayer({ + required html.VideoElement videoElement, + @visibleForTesting StreamController? eventController, + }) : _videoElement = videoElement, + _eventController = eventController ?? StreamController(); + + final StreamController _eventController; + final html.VideoElement _videoElement; + + bool _isInitialized = false; + bool _isBuffering = false; + + /// Returns the [Stream] of [VideoEvent]s from the inner [html.VideoElement]. + Stream get events => _eventController.stream; + + /// Initializes the wrapped [html.VideoElement]. + /// + /// This method sets the required DOM attributes so videos can [play] programmatically, + /// and attaches listeners to the internal events from the [html.VideoElement] + /// to react to them / expose them through the [VideoPlayer.events] stream. + void initialize() { + _videoElement + ..autoplay = false + ..controls = false; + + // Allows Safari iOS to play the video inline + _videoElement.setAttribute('playsinline', 'true'); + + // Set autoplay to false since most browsers won't autoplay a video unless it is muted + _videoElement.setAttribute('autoplay', 'false'); + + _videoElement.onCanPlay.listen((dynamic _) { + if (!_isInitialized) { + _isInitialized = true; + _sendInitialized(); + } + }); + + _videoElement.onCanPlayThrough.listen((dynamic _) { + setBuffering(false); + }); + + _videoElement.onPlaying.listen((dynamic _) { + setBuffering(false); + }); + + _videoElement.onWaiting.listen((dynamic _) { + setBuffering(true); + _sendBufferingRangesUpdate(); + }); + + // The error event fires when some form of error occurs while attempting to load or perform the media. + _videoElement.onError.listen((html.Event _) { + setBuffering(false); + // The Event itself (_) doesn't contain info about the actual error. + // We need to look at the HTMLMediaElement.error. + // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error + final html.MediaError error = _videoElement.error!; + _eventController.addError(PlatformException( + code: _kErrorValueToErrorName[error.code]!, + message: error.message != '' ? error.message : _kDefaultErrorMessage, + details: _kErrorValueToErrorDescription[error.code], + )); + }); + + _videoElement.onEnded.listen((dynamic _) { + setBuffering(false); + _eventController.add(VideoEvent(eventType: VideoEventType.completed)); + }); + } + + /// Attempts to play the video. + /// + /// If this method is called programmatically (without user interaction), it + /// might fail unless the video is completely muted (or it has no Audio tracks). + /// + /// When called from some user interaction (a tap on a button), the above + /// limitation should disappear. + Future play() { + return _videoElement.play().catchError((Object e) { + // play() attempts to begin playback of the media. It returns + // a Promise which can get rejected in case of failure to begin + // playback for any reason, such as permission issues. + // The rejection handler is called with a DomException. + // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play + final html.DomException exception = e as html.DomException; + _eventController.addError(PlatformException( + code: exception.name, + message: exception.message, + )); + }, test: (Object e) => e is html.DomException); + } + + /// Pauses the video in the current position. + void pause() { + _videoElement.pause(); + } + + /// Controls whether the video should start again after it finishes. + void setLooping(bool value) { + _videoElement.loop = value; + } + + /// Sets the volume at which the media will be played. + /// + /// Values must fall between 0 and 1, where 0 is muted and 1 is the loudest. + /// + /// When volume is set to 0, the `muted` property is also applied to the + /// [html.VideoElement]. This is required for auto-play on the web. + void setVolume(double volume) { + assert(volume >= 0 && volume <= 1); + + // TODO(ditman): Do we need to expose a "muted" API? + // https://github.com/flutter/flutter/issues/60721 + _videoElement.muted = !(volume > 0.0); + _videoElement.volume = volume; + } + + /// Sets the playback `speed`. + /// + /// A `speed` of 1.0 is "normal speed," values lower than 1.0 make the media + /// play slower than normal, higher values make it play faster. + /// + /// `speed` cannot be negative. + /// + /// The audio is muted when the fast forward or slow motion is outside a useful + /// range (for example, Gecko mutes the sound outside the range 0.25 to 4.0). + /// + /// The pitch of the audio is corrected by default. + void setPlaybackSpeed(double speed) { + assert(speed > 0); + + _videoElement.playbackRate = speed; + } + + /// Moves the playback head to a new `position`. + /// + /// `position` cannot be negative. + void seekTo(Duration position) { + assert(!position.isNegative); + + _videoElement.currentTime = position.inMilliseconds.toDouble() / 1000; + } + + /// Returns the current playback head position as a [Duration]. + Duration getPosition() { + _sendBufferingRangesUpdate(); + return Duration(milliseconds: (_videoElement.currentTime * 1000).round()); + } + + /// Disposes of the current [html.VideoElement]. + void dispose() { + _videoElement.removeAttribute('src'); + _videoElement.load(); + } + + // Sends an [VideoEventType.initialized] [VideoEvent] with info about the wrapped video. + void _sendInitialized() { + final Duration? duration = !_videoElement.duration.isNaN + ? Duration( + milliseconds: (_videoElement.duration * 1000).round(), + ) + : null; + + final Size? size = !_videoElement.videoHeight.isNaN + ? Size( + _videoElement.videoWidth.toDouble(), + _videoElement.videoHeight.toDouble(), + ) + : null; + + _eventController.add( + VideoEvent( + eventType: VideoEventType.initialized, + duration: duration, + size: size, + ), + ); + } + + /// Caches the current "buffering" state of the video. + /// + /// If the current buffering state is different from the previous one + /// ([_isBuffering]), this dispatches a [VideoEvent]. + @visibleForTesting + void setBuffering(bool buffering) { + if (_isBuffering != buffering) { + _isBuffering = buffering; + _eventController.add(VideoEvent( + eventType: _isBuffering + ? VideoEventType.bufferingStart + : VideoEventType.bufferingEnd, + )); + } + } + + // Broadcasts the [html.VideoElement.buffered] status through the [events] stream. + void _sendBufferingRangesUpdate() { + _eventController.add(VideoEvent( + buffered: _toDurationRange(_videoElement.buffered), + eventType: VideoEventType.bufferingUpdate, + )); + } + + // Converts from [html.TimeRanges] to our own List. + List _toDurationRange(html.TimeRanges buffered) { + final List durationRange = []; + for (int i = 0; i < buffered.length; i++) { + durationRange.add(DurationRange( + Duration(milliseconds: (buffered.start(i) * 1000).round()), + Duration(milliseconds: (buffered.end(i) * 1000).round()), + )); + } + return durationRange; + } +} diff --git a/packages/video_player/video_player_web/lib/video_player_web.dart b/packages/video_player/video_player_web/lib/video_player_web.dart index a676850f3488..e52fd83de79e 100644 --- a/packages/video_player/video_player_web/lib/video_player_web.dart +++ b/packages/video_player/video_player_web/lib/video_player_web.dart @@ -6,34 +6,11 @@ import 'dart:async'; import 'dart:html'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; import 'src/shims/dart_ui.dart' as ui; - -// An error code value to error name Map. -// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code -const Map _kErrorValueToErrorName = { - 1: 'MEDIA_ERR_ABORTED', - 2: 'MEDIA_ERR_NETWORK', - 3: 'MEDIA_ERR_DECODE', - 4: 'MEDIA_ERR_SRC_NOT_SUPPORTED', -}; - -// An error code value to description Map. -// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code -const Map _kErrorValueToErrorDescription = { - 1: 'The user canceled the fetching of the video.', - 2: 'A network error occurred while fetching the video, despite having previously been available.', - 3: 'An error occurred while trying to decode the video, despite having previously been determined to be usable.', - 4: 'The video has been found to be unsuitable (missing or in a format not supported by your browser).', -}; - -// The default error message, when the error is an empty string -// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/message -const String _kDefaultErrorMessage = - 'No further diagnostic information can be determined or provided.'; +import 'src/video_player.dart'; /// The web implementation of [VideoPlayerPlatform]. /// @@ -44,8 +21,10 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { VideoPlayerPlatform.instance = VideoPlayerPlugin(); } - final Map _videoPlayers = {}; + // Map of textureId -> VideoPlayer instances + final Map _videoPlayers = {}; + // Simulate the native "textureId". int _textureCounter = 1; @override @@ -55,13 +34,13 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { @override Future dispose(int textureId) async { - _videoPlayers[textureId]!.dispose(); + _player(textureId).dispose(); _videoPlayers.remove(textureId); return; } void _disposeAllPlayers() { - for (final _VideoPlayer videoPlayer in _videoPlayers.values) { + for (final VideoPlayer videoPlayer in _videoPlayers.values) { videoPlayer.dispose(); } _videoPlayers.clear(); @@ -69,8 +48,7 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { @override Future create(DataSource dataSource) async { - final int textureId = _textureCounter; - _textureCounter++; + final int textureId = _textureCounter++; late String uri; switch (dataSource.sourceType) { @@ -95,58 +73,69 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { 'web implementation of video_player cannot play content uri')); } - final _VideoPlayer player = _VideoPlayer( - uri: uri, - textureId: textureId, - ); + final VideoElement videoElement = VideoElement() + ..id = 'videoElement-$textureId' + ..src = uri + ..style.border = 'none' + ..style.height = '100%' + ..style.width = '100%'; - player.initialize(); + // TODO(hterkelsen): Use initialization parameters once they are available + ui.platformViewRegistry.registerViewFactory( + 'videoPlayer-$textureId', (int viewId) => videoElement); + + final VideoPlayer player = VideoPlayer(videoElement: videoElement) + ..initialize(); _videoPlayers[textureId] = player; + return textureId; } @override Future setLooping(int textureId, bool looping) async { - return _videoPlayers[textureId]!.setLooping(looping); + return _player(textureId).setLooping(looping); } @override Future play(int textureId) async { - return _videoPlayers[textureId]!.play(); + return _player(textureId).play(); } @override Future pause(int textureId) async { - return _videoPlayers[textureId]!.pause(); + return _player(textureId).pause(); } @override Future setVolume(int textureId, double volume) async { - return _videoPlayers[textureId]!.setVolume(volume); + return _player(textureId).setVolume(volume); } @override Future setPlaybackSpeed(int textureId, double speed) async { - assert(speed > 0); - - return _videoPlayers[textureId]!.setPlaybackSpeed(speed); + return _player(textureId).setPlaybackSpeed(speed); } @override Future seekTo(int textureId, Duration position) async { - return _videoPlayers[textureId]!.seekTo(position); + return _player(textureId).seekTo(position); } @override Future getPosition(int textureId) async { - _videoPlayers[textureId]!.sendBufferingUpdate(); - return _videoPlayers[textureId]!.getPosition(); + return _player(textureId).getPosition(); } @override Stream videoEventsFor(int textureId) { - return _videoPlayers[textureId]!.eventController.stream; + return _player(textureId).events; + } + + // Retrieves a [VideoPlayer] by its internal `id`. + // It must have been created earlier from the [create] method. + VideoPlayer _player(int id) { + return _videoPlayers[id]!; } @override @@ -158,171 +147,3 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { @override Future setMixWithOthers(bool mixWithOthers) => Future.value(); } - -class _VideoPlayer { - _VideoPlayer({required this.uri, required this.textureId}); - - final StreamController eventController = - StreamController(); - - final String uri; - final int textureId; - late VideoElement videoElement; - bool isInitialized = false; - bool isBuffering = false; - - void setBuffering(bool buffering) { - if (isBuffering != buffering) { - isBuffering = buffering; - eventController.add(VideoEvent( - eventType: isBuffering - ? VideoEventType.bufferingStart - : VideoEventType.bufferingEnd)); - } - } - - void initialize() { - videoElement = VideoElement() - ..src = uri - ..autoplay = false - ..controls = false - ..style.border = 'none' - ..style.height = '100%' - ..style.width = '100%'; - - // Allows Safari iOS to play the video inline - videoElement.setAttribute('playsinline', 'true'); - - // Set autoplay to false since most browsers won't autoplay a video unless it is muted - videoElement.setAttribute('autoplay', 'false'); - - // TODO(hterkelsen): Use initialization parameters once they are available - ui.platformViewRegistry.registerViewFactory( - 'videoPlayer-$textureId', (int viewId) => videoElement); - - videoElement.onCanPlay.listen((dynamic _) { - if (!isInitialized) { - isInitialized = true; - sendInitialized(); - } - setBuffering(false); - }); - - videoElement.onCanPlayThrough.listen((dynamic _) { - setBuffering(false); - }); - - videoElement.onPlaying.listen((dynamic _) { - setBuffering(false); - }); - - videoElement.onWaiting.listen((dynamic _) { - setBuffering(true); - sendBufferingUpdate(); - }); - - // The error event fires when some form of error occurs while attempting to load or perform the media. - videoElement.onError.listen((Event _) { - setBuffering(false); - // The Event itself (_) doesn't contain info about the actual error. - // We need to look at the HTMLMediaElement.error. - // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error - final MediaError error = videoElement.error!; - eventController.addError(PlatformException( - code: _kErrorValueToErrorName[error.code]!, - message: error.message != '' ? error.message : _kDefaultErrorMessage, - details: _kErrorValueToErrorDescription[error.code], - )); - }); - - videoElement.onEnded.listen((dynamic _) { - setBuffering(false); - eventController.add(VideoEvent(eventType: VideoEventType.completed)); - }); - } - - void sendBufferingUpdate() { - eventController.add(VideoEvent( - buffered: _toDurationRange(videoElement.buffered), - eventType: VideoEventType.bufferingUpdate, - )); - } - - Future play() { - return videoElement.play().catchError((Object e) { - // play() attempts to begin playback of the media. It returns - // a Promise which can get rejected in case of failure to begin - // playback for any reason, such as permission issues. - // The rejection handler is called with a DomException. - // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play - final DomException exception = e as DomException; - eventController.addError(PlatformException( - code: exception.name, - message: exception.message, - )); - }, test: (Object e) => e is DomException); - } - - void pause() { - videoElement.pause(); - } - - void setLooping(bool value) { - videoElement.loop = value; - } - - void setVolume(double value) { - // TODO(ditman): Do we need to expose a "muted" API? https://github.com/flutter/flutter/issues/60721 - if (value > 0.0) { - videoElement.muted = false; - } else { - videoElement.muted = true; - } - videoElement.volume = value; - } - - void setPlaybackSpeed(double speed) { - assert(speed > 0); - - videoElement.playbackRate = speed; - } - - void seekTo(Duration position) { - videoElement.currentTime = position.inMilliseconds.toDouble() / 1000; - } - - Duration getPosition() { - return Duration(milliseconds: (videoElement.currentTime * 1000).round()); - } - - void sendInitialized() { - eventController.add( - VideoEvent( - eventType: VideoEventType.initialized, - duration: Duration( - milliseconds: (videoElement.duration * 1000).round(), - ), - size: Size( - videoElement.videoWidth.toDouble(), - videoElement.videoHeight.toDouble(), - ), - ), - ); - } - - void dispose() { - videoElement.removeAttribute('src'); - videoElement.load(); - } - - List _toDurationRange(TimeRanges buffered) { - final List durationRange = []; - for (int i = 0; i < buffered.length; i++) { - durationRange.add(DurationRange( - Duration(milliseconds: (buffered.start(i) * 1000).round()), - Duration(milliseconds: (buffered.end(i) * 1000).round()), - )); - } - return durationRange; - } -} diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index 69a2df4e99e4..064517e1f264 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_web description: Web platform implementation of video_player. repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.0.7 +version: 2.0.8 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 34b56d61bea8..7a56f3f176d0 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Removes unnecessary imports. + +## 3.0.2 + +* Migrates deprecated `Scaffold.showSnackBar` to `ScaffoldMessenger` in example app. +* Adds OS version support information to README. + ## 3.0.1 * Removes a duplicate Android-specific integration test. diff --git a/packages/webview_flutter/webview_flutter/README.md b/packages/webview_flutter/webview_flutter/README.md index 8387eb685b7d..ffe91441326d 100644 --- a/packages/webview_flutter/webview_flutter/README.md +++ b/packages/webview_flutter/webview_flutter/README.md @@ -7,6 +7,10 @@ A Flutter plugin that provides a WebView widget. On iOS the WebView widget is backed by a [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview); On Android the WebView widget is backed by a [WebView](https://developer.android.com/reference/android/webkit/WebView). +| | Android | iOS | +|-------------|----------------|------| +| **Support** | SDK 19+ or 20+ | 9.0+ | + ## Usage Add `webview_flutter` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). If you are targeting Android, make sure to read the *Android Platform Views* section below to choose the platform view mode that best suits your needs. @@ -91,4 +95,4 @@ follow the steps described in the [Enabling Material Components instructions](ht ### Setting custom headers on POST requests Currently, setting custom headers when making a post request with the WebViewController's `loadRequest` method is not supported on Android. -If you require this functionality, a workaround is to make the request manually, and then load the response data using `loadHTMLString` instead. \ No newline at end of file +If you require this functionality, a workaround is to make the request manually, and then load the response data using `loadHTMLString` instead. diff --git a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart index 02ee1976eb67..ba321264ee1e 100644 --- a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -14,10 +14,8 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:webview_flutter/platform_interface.dart'; import 'package:webview_flutter/webview_flutter.dart'; Future main() async { @@ -1217,6 +1215,52 @@ Future main() async { }, skip: _skipDueToIssue86757, ); + + testWidgets( + 'clearCache should clear local storage', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer onPageFinished = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (_) => onPageFinished.complete(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await onPageFinished.future; + + await controller.runJavascript('localStorage.setItem("myCat", "Tom");'); + + expect( + controller.runJavascriptReturningResult( + 'localStorage.getItem("myCat");', + ), + completion(_webviewString('Tom')), + ); + + await controller.clearCache(); + + expect( + controller.runJavascriptReturningResult( + 'localStorage.getItem("myCat");', + ), + completion(_webviewNull()), + ); + }, + // TODO(bparrishMines): Unskip once https://github.com/flutter/plugins/pull/5086 lands and is published. + skip: Platform.isAndroid, + ); } // JavaScript booleans evaluate to different string values on Android and iOS. @@ -1228,6 +1272,24 @@ String _webviewBool(bool value) { return value ? 'true' : 'false'; } +// JavaScript `null` evaluate to different string values on Android and iOS. +// This utility method returns the string boolean value of the current platform. +String _webviewNull() { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return ''; + } + return 'null'; +} + +// JavaScript String evaluate to different string values on Android and iOS. +// This utility method returns the string boolean value of the current platform. +String _webviewString(String value) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return value; + } + return '"$value"'; +} + /// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. Future _getUserAgent(WebViewController controller) async { return _runJavascriptReturningResult(controller, 'navigator.userAgent;'); diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index 88c240a7316c..51080be01df0 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -13,7 +13,7 @@ import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:webview_flutter/webview_flutter.dart'; -void main() => runApp(MaterialApp(home: WebViewExample())); +void main() => runApp(const MaterialApp(home: WebViewExample())); const String kNavigationExamplePage = ''' @@ -71,6 +71,10 @@ const String kTransparentBackgroundPage = ''' '''; class WebViewExample extends StatefulWidget { + const WebViewExample({this.cookieManager}); + + final CookieManager? cookieManager; + @override _WebViewExampleState createState() => _WebViewExampleState(); } @@ -96,42 +100,38 @@ class _WebViewExampleState extends State { // This drop down menu demonstrates that Flutter widgets can be shown over the web view. actions: [ NavigationControls(_controller.future), - SampleMenu(_controller.future), + SampleMenu(_controller.future, widget.cookieManager), ], ), - // We're using a Builder here so we have a context that is below the Scaffold - // to allow calling Scaffold.of(context) so we can show a snackbar. - body: Builder(builder: (BuildContext context) { - return WebView( - initialUrl: 'https://flutter.dev', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController webViewController) { - _controller.complete(webViewController); - }, - onProgress: (int progress) { - print('WebView is loading (progress : $progress%)'); - }, - javascriptChannels: { - _toasterJavascriptChannel(context), - }, - navigationDelegate: (NavigationRequest request) { - if (request.url.startsWith('https://www.youtube.com/')) { - print('blocking navigation to $request}'); - return NavigationDecision.prevent; - } - print('allowing navigation to $request'); - return NavigationDecision.navigate; - }, - onPageStarted: (String url) { - print('Page started loading: $url'); - }, - onPageFinished: (String url) { - print('Page finished loading: $url'); - }, - gestureNavigationEnabled: true, - backgroundColor: const Color(0x00000000), - ); - }), + body: WebView( + initialUrl: 'https://flutter.dev', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + _controller.complete(webViewController); + }, + onProgress: (int progress) { + print('WebView is loading (progress : $progress%)'); + }, + javascriptChannels: { + _toasterJavascriptChannel(context), + }, + navigationDelegate: (NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $request}'); + return NavigationDecision.prevent; + } + print('allowing navigation to $request'); + return NavigationDecision.navigate; + }, + onPageStarted: (String url) { + print('Page started loading: $url'); + }, + onPageFinished: (String url) { + print('Page finished loading: $url'); + }, + gestureNavigationEnabled: true, + backgroundColor: const Color(0x00000000), + ), floatingActionButton: favoriteButton(), ); } @@ -140,8 +140,7 @@ class _WebViewExampleState extends State { return JavascriptChannel( name: 'Toaster', onMessageReceived: (JavascriptMessage message) { - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message.message)), ); }); @@ -152,19 +151,24 @@ class _WebViewExampleState extends State { future: _controller.future, builder: (BuildContext context, AsyncSnapshot controller) { - if (controller.hasData) { - return FloatingActionButton( - onPressed: () async { - final String url = (await controller.data!.currentUrl())!; - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( - SnackBar(content: Text('Favorited $url')), - ); - }, - child: const Icon(Icons.favorite), - ); - } - return Container(); + return FloatingActionButton( + onPressed: () async { + String? url; + if (controller.hasData) { + url = (await controller.data!.currentUrl())!; + } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + controller.hasData + ? 'Favorited $url' + : 'Unable to favorite', + ), + ), + ); + }, + child: const Icon(Icons.favorite), + ); }); } } @@ -186,10 +190,11 @@ enum MenuOptions { } class SampleMenu extends StatelessWidget { - SampleMenu(this.controller); + SampleMenu(this.controller, CookieManager? cookieManager) + : cookieManager = cookieManager ?? CookieManager(); final Future controller; - final CookieManager cookieManager = CookieManager(); + late final CookieManager cookieManager; @override Widget build(BuildContext context) { @@ -315,8 +320,7 @@ class SampleMenu extends StatelessWidget { WebViewController controller, BuildContext context) async { final String cookies = await controller.runJavascriptReturningResult('document.cookie'); - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(SnackBar( + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Column( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, @@ -332,8 +336,7 @@ class SampleMenu extends StatelessWidget { WebViewController controller, BuildContext context) async { await controller.runJavascript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(const SnackBar( + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Added a test entry to cache.'), )); } @@ -348,8 +351,7 @@ class SampleMenu extends StatelessWidget { Future _onClearCache( WebViewController controller, BuildContext context) async { await controller.clearCache(); - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(const SnackBar( + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Cache cleared.'), )); } @@ -360,8 +362,7 @@ class SampleMenu extends StatelessWidget { if (!hadCookies) { message = 'There are no cookies.'; } - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(SnackBar( + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(message), )); } @@ -375,7 +376,7 @@ class SampleMenu extends StatelessWidget { Future _onSetCookie( WebViewController controller, BuildContext context) async { - await CookieManager().setCookie( + await cookieManager.setCookie( const WebViewCookie( name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), ); @@ -466,8 +467,7 @@ class NavigationControls extends StatelessWidget { if (await controller!.canGoBack()) { await controller.goBack(); } else { - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('No back history item')), ); return; @@ -482,8 +482,7 @@ class NavigationControls extends StatelessWidget { if (await controller!.canGoForward()) { await controller.goForward(); } else { - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('No forward history item')), ); diff --git a/packages/webview_flutter/webview_flutter/example/test/main_test.dart b/packages/webview_flutter/webview_flutter/example/test/main_test.dart new file mode 100644 index 000000000000..867633366e1a --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/test/main_test.dart @@ -0,0 +1,39 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_example/main.dart'; + +void main() { + testWidgets('Test snackbar from ScaffoldMessenger', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: WebViewExample(cookieManager: FakeCookieManager()), + ), + ); + expect(find.byIcon(Icons.favorite), findsOneWidget); + await tester.tap(find.byIcon(Icons.favorite)); + await tester.pump(); + expect(find.byType(SnackBar), findsOneWidget); + }); +} + +class FakeCookieManager implements CookieManager { + factory FakeCookieManager() { + return _instance ??= FakeCookieManager._(); + } + + FakeCookieManager._(); + + static FakeCookieManager? _instance; + + @override + Future clearCookies() => throw UnimplementedError(); + + @override + Future setCookie(WebViewCookie cookie) => throw UnimplementedError(); +} diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/webview.dart index 6a24d3d4cb2d..697eb487b953 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview.dart @@ -7,15 +7,12 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; import 'package:webview_flutter_android/webview_surface_android.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; -import '../platform_interface.dart'; - /// Optional callback invoked when a web view is first created. [controller] is /// the [WebViewController] for the created web view. typedef WebViewCreatedCallback = void Function(WebViewController controller); diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 322f4ada3b56..10350984ce9a 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.0.1 +version: 3.0.2 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 12d20b00f534..edaa0883713f 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,6 +1,20 @@ ## NEXT +* Removes unnecessary imports. + +## 2.8.6 + +* Updates pigeon developer dependency to the latest version which adds support for null safety. + +## 2.8.5 + +* Migrates deprecated `Scaffold.showSnackBar` to `ScaffoldMessenger` in example app. + +## 2.8.4 + * Fixes bug preventing `mockito` code generation for tests. +* Fixes regression where local storage wasn't cleared when `WebViewController.clearCache` was + called. ## 2.8.3 diff --git a/packages/webview_flutter/webview_flutter_android/README.md b/packages/webview_flutter/webview_flutter_android/README.md index 020f1fcfbbf1..04cbde292356 100644 --- a/packages/webview_flutter/webview_flutter_android/README.md +++ b/packages/webview_flutter/webview_flutter_android/README.md @@ -9,7 +9,10 @@ normally. This package will be automatically included in your app when you do. ## Contributing -This package uses [pigeon][3] to generate the communication layer between Flutter and the host platform (Android). The communication interface is defined in the `pigeons/android_webview.dart` file. After editing the communication interface regenerate the communication layer by running the `./generatePigeons.sh` shell script. +This package uses [pigeon][3] to generate the communication layer between Flutter and the host +platform (Android). The communication interface is defined in the `pigeons/android_webview.dart` +file. After editing the communication interface regenerate the communication layer by running +`flutter pub run pigeon --input pigeons/android_webview.dart`. Due to [flutter/flutter#97744](https://github.com/flutter/flutter/issues/97744), the generated test pigeon file needs one of its imports updated to properly work with `mockito`. @@ -26,7 +29,8 @@ to import 'package:webview_flutter_android/src/android_webview.pigeon.dart'; ``` -Besides [pigeon][3] this package also uses [mockito][4] to generate mock objects for testing purposes. To generate the mock objects run the following command: +Besides [pigeon][3] this package also uses [mockito][4] to generate mock objects for testing +purposes. To generate the mock objects run the following command: ```bash flutter packages pub run build_runner build --delete-conflicting-outputs ``` diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java index 15b78b718115..2e163311d6d4 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java @@ -1,12 +1,14 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - -// Autogenerated from Pigeon (v1.0.9), do not edit directly. +// Autogenerated from Pigeon (v3.0.3), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.webviewflutter; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.flutter.plugin.common.BasicMessageChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MessageCodec; @@ -25,66 +27,140 @@ public class GeneratedAndroidWebView { /** Generated class from Pigeon that represents data sent in messages. */ public static class WebResourceRequestData { - private String url; + private @NonNull String url; - public String getUrl() { + public @NonNull String getUrl() { return url; } - public void setUrl(String setterArg) { + public void setUrl(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"url\" is null."); + } this.url = setterArg; } - private Boolean isForMainFrame; + private @NonNull Boolean isForMainFrame; - public Boolean getIsForMainFrame() { + public @NonNull Boolean getIsForMainFrame() { return isForMainFrame; } - public void setIsForMainFrame(Boolean setterArg) { + public void setIsForMainFrame(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"isForMainFrame\" is null."); + } this.isForMainFrame = setterArg; } - private Boolean isRedirect; + private @Nullable Boolean isRedirect; - public Boolean getIsRedirect() { + public @Nullable Boolean getIsRedirect() { return isRedirect; } - public void setIsRedirect(Boolean setterArg) { + public void setIsRedirect(@Nullable Boolean setterArg) { this.isRedirect = setterArg; } - private Boolean hasGesture; + private @NonNull Boolean hasGesture; - public Boolean getHasGesture() { + public @NonNull Boolean getHasGesture() { return hasGesture; } - public void setHasGesture(Boolean setterArg) { + public void setHasGesture(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"hasGesture\" is null."); + } this.hasGesture = setterArg; } - private String method; + private @NonNull String method; - public String getMethod() { + public @NonNull String getMethod() { return method; } - public void setMethod(String setterArg) { + public void setMethod(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"method\" is null."); + } this.method = setterArg; } - private Map requestHeaders; + private @NonNull Map requestHeaders; - public Map getRequestHeaders() { + public @NonNull Map getRequestHeaders() { return requestHeaders; } - public void setRequestHeaders(Map setterArg) { + public void setRequestHeaders(@NonNull Map setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"requestHeaders\" is null."); + } this.requestHeaders = setterArg; } + /** Constructor is private to enforce null safety; use Builder. */ + private WebResourceRequestData() {} + + public static final class Builder { + private @Nullable String url; + + public @NonNull Builder setUrl(@NonNull String setterArg) { + this.url = setterArg; + return this; + } + + private @Nullable Boolean isForMainFrame; + + public @NonNull Builder setIsForMainFrame(@NonNull Boolean setterArg) { + this.isForMainFrame = setterArg; + return this; + } + + private @Nullable Boolean isRedirect; + + public @NonNull Builder setIsRedirect(@Nullable Boolean setterArg) { + this.isRedirect = setterArg; + return this; + } + + private @Nullable Boolean hasGesture; + + public @NonNull Builder setHasGesture(@NonNull Boolean setterArg) { + this.hasGesture = setterArg; + return this; + } + + private @Nullable String method; + + public @NonNull Builder setMethod(@NonNull String setterArg) { + this.method = setterArg; + return this; + } + + private @Nullable Map requestHeaders; + + public @NonNull Builder setRequestHeaders(@NonNull Map setterArg) { + this.requestHeaders = setterArg; + return this; + } + + public @NonNull WebResourceRequestData build() { + WebResourceRequestData pigeonReturn = new WebResourceRequestData(); + pigeonReturn.setUrl(url); + pigeonReturn.setIsForMainFrame(isForMainFrame); + pigeonReturn.setIsRedirect(isRedirect); + pigeonReturn.setHasGesture(hasGesture); + pigeonReturn.setMethod(method); + pigeonReturn.setRequestHeaders(requestHeaders); + return pigeonReturn; + } + } + + @NonNull Map toMap() { Map toMapResult = new HashMap<>(); toMapResult.put("url", url); @@ -96,46 +172,79 @@ Map toMap() { return toMapResult; } - static WebResourceRequestData fromMap(Map map) { - WebResourceRequestData fromMapResult = new WebResourceRequestData(); + static @NonNull WebResourceRequestData fromMap(@NonNull Map map) { + WebResourceRequestData pigeonResult = new WebResourceRequestData(); Object url = map.get("url"); - fromMapResult.url = (String) url; + pigeonResult.setUrl((String) url); Object isForMainFrame = map.get("isForMainFrame"); - fromMapResult.isForMainFrame = (Boolean) isForMainFrame; + pigeonResult.setIsForMainFrame((Boolean) isForMainFrame); Object isRedirect = map.get("isRedirect"); - fromMapResult.isRedirect = (Boolean) isRedirect; + pigeonResult.setIsRedirect((Boolean) isRedirect); Object hasGesture = map.get("hasGesture"); - fromMapResult.hasGesture = (Boolean) hasGesture; + pigeonResult.setHasGesture((Boolean) hasGesture); Object method = map.get("method"); - fromMapResult.method = (String) method; + pigeonResult.setMethod((String) method); Object requestHeaders = map.get("requestHeaders"); - fromMapResult.requestHeaders = (Map) requestHeaders; - return fromMapResult; + pigeonResult.setRequestHeaders((Map) requestHeaders); + return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ public static class WebResourceErrorData { - private Long errorCode; + private @NonNull Long errorCode; - public Long getErrorCode() { + public @NonNull Long getErrorCode() { return errorCode; } - public void setErrorCode(Long setterArg) { + public void setErrorCode(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"errorCode\" is null."); + } this.errorCode = setterArg; } - private String description; + private @NonNull String description; - public String getDescription() { + public @NonNull String getDescription() { return description; } - public void setDescription(String setterArg) { + public void setDescription(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"description\" is null."); + } this.description = setterArg; } + /** Constructor is private to enforce null safety; use Builder. */ + private WebResourceErrorData() {} + + public static final class Builder { + private @Nullable Long errorCode; + + public @NonNull Builder setErrorCode(@NonNull Long setterArg) { + this.errorCode = setterArg; + return this; + } + + private @Nullable String description; + + public @NonNull Builder setDescription(@NonNull String setterArg) { + this.description = setterArg; + return this; + } + + public @NonNull WebResourceErrorData build() { + WebResourceErrorData pigeonReturn = new WebResourceErrorData(); + pigeonReturn.setErrorCode(errorCode); + pigeonReturn.setDescription(description); + return pigeonReturn; + } + } + + @NonNull Map toMap() { Map toMapResult = new HashMap<>(); toMapResult.put("errorCode", errorCode); @@ -143,16 +252,16 @@ Map toMap() { return toMapResult; } - static WebResourceErrorData fromMap(Map map) { - WebResourceErrorData fromMapResult = new WebResourceErrorData(); + static @NonNull WebResourceErrorData fromMap(@NonNull Map map) { + WebResourceErrorData pigeonResult = new WebResourceErrorData(); Object errorCode = map.get("errorCode"); - fromMapResult.errorCode = + pigeonResult.setErrorCode( (errorCode == null) ? null - : ((errorCode instanceof Integer) ? (Integer) errorCode : (Long) errorCode); + : ((errorCode instanceof Integer) ? (Integer) errorCode : (Long) errorCode)); Object description = map.get("description"); - fromMapResult.description = (String) description; - return fromMapResult; + pigeonResult.setDescription((String) description); + return pigeonResult; } } @@ -172,7 +281,7 @@ private CookieManagerHostApiCodec() {} public interface CookieManagerHostApi { void clearCookies(Result result); - void setCookie(String url, String value); + void setCookie(@NonNull String url, @NonNull String value); /** The codec used by CookieManagerHostApi. */ static MessageCodec getCodec() { @@ -258,63 +367,76 @@ private WebViewHostApiCodec() {} /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebViewHostApi { - void create(Long instanceId, Boolean useHybridComposition); + void create(@NonNull Long instanceId, @NonNull Boolean useHybridComposition); - void dispose(Long instanceId); + void dispose(@NonNull Long instanceId); - void loadData(Long instanceId, String data, String mimeType, String encoding); + void loadData( + @NonNull Long instanceId, + @NonNull String data, + @Nullable String mimeType, + @Nullable String encoding); void loadDataWithBaseUrl( - Long instanceId, - String baseUrl, - String data, - String mimeType, - String encoding, - String historyUrl); + @NonNull Long instanceId, + @Nullable String baseUrl, + @NonNull String data, + @Nullable String mimeType, + @Nullable String encoding, + @Nullable String historyUrl); - void loadUrl(Long instanceId, String url, Map headers); + void loadUrl( + @NonNull Long instanceId, @NonNull String url, @NonNull Map headers); - void postUrl(Long instanceId, String url, byte[] data); + void postUrl(@NonNull Long instanceId, @NonNull String url, @NonNull byte[] data); - String getUrl(Long instanceId); + @Nullable + String getUrl(@NonNull Long instanceId); - Boolean canGoBack(Long instanceId); + @NonNull + Boolean canGoBack(@NonNull Long instanceId); - Boolean canGoForward(Long instanceId); + @NonNull + Boolean canGoForward(@NonNull Long instanceId); - void goBack(Long instanceId); + void goBack(@NonNull Long instanceId); - void goForward(Long instanceId); + void goForward(@NonNull Long instanceId); - void reload(Long instanceId); + void reload(@NonNull Long instanceId); - void clearCache(Long instanceId, Boolean includeDiskFiles); + void clearCache(@NonNull Long instanceId, @NonNull Boolean includeDiskFiles); - void evaluateJavascript(Long instanceId, String javascriptString, Result result); + void evaluateJavascript( + @NonNull Long instanceId, @NonNull String javascriptString, Result result); - String getTitle(Long instanceId); + @Nullable + String getTitle(@NonNull Long instanceId); - void scrollTo(Long instanceId, Long x, Long y); + void scrollTo(@NonNull Long instanceId, @NonNull Long x, @NonNull Long y); - void scrollBy(Long instanceId, Long x, Long y); + void scrollBy(@NonNull Long instanceId, @NonNull Long x, @NonNull Long y); - Long getScrollX(Long instanceId); + @NonNull + Long getScrollX(@NonNull Long instanceId); - Long getScrollY(Long instanceId); + @NonNull + Long getScrollY(@NonNull Long instanceId); - void setWebContentsDebuggingEnabled(Boolean enabled); + void setWebContentsDebuggingEnabled(@NonNull Boolean enabled); - void setWebViewClient(Long instanceId, Long webViewClientInstanceId); + void setWebViewClient(@NonNull Long instanceId, @NonNull Long webViewClientInstanceId); - void addJavaScriptChannel(Long instanceId, Long javaScriptChannelInstanceId); + void addJavaScriptChannel(@NonNull Long instanceId, @NonNull Long javaScriptChannelInstanceId); - void removeJavaScriptChannel(Long instanceId, Long javaScriptChannelInstanceId); + void removeJavaScriptChannel( + @NonNull Long instanceId, @NonNull Long javaScriptChannelInstanceId); - void setDownloadListener(Long instanceId, Long listenerInstanceId); + void setDownloadListener(@NonNull Long instanceId, @Nullable Long listenerInstanceId); - void setWebChromeClient(Long instanceId, Long clientInstanceId); + void setWebChromeClient(@NonNull Long instanceId, @Nullable Long clientInstanceId); - void setBackgroundColor(Long instanceId, Long color); + void setBackgroundColor(@NonNull Long instanceId, @NonNull Long color); /** The codec used by WebViewHostApi. */ static MessageCodec getCodec() { @@ -341,7 +463,9 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (useHybridCompositionArg == null) { throw new NullPointerException("useHybridCompositionArg unexpectedly null."); } - api.create(instanceIdArg.longValue(), useHybridCompositionArg); + api.create( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + useHybridCompositionArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -366,7 +490,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - api.dispose(instanceIdArg.longValue()); + api.dispose((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -396,14 +520,12 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { throw new NullPointerException("dataArg unexpectedly null."); } String mimeTypeArg = (String) args.get(2); - if (mimeTypeArg == null) { - throw new NullPointerException("mimeTypeArg unexpectedly null."); - } String encodingArg = (String) args.get(3); - if (encodingArg == null) { - throw new NullPointerException("encodingArg unexpectedly null."); - } - api.loadData(instanceIdArg.longValue(), dataArg, mimeTypeArg, encodingArg); + api.loadData( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + dataArg, + mimeTypeArg, + encodingArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -431,27 +553,15 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { throw new NullPointerException("instanceIdArg unexpectedly null."); } String baseUrlArg = (String) args.get(1); - if (baseUrlArg == null) { - throw new NullPointerException("baseUrlArg unexpectedly null."); - } String dataArg = (String) args.get(2); if (dataArg == null) { throw new NullPointerException("dataArg unexpectedly null."); } String mimeTypeArg = (String) args.get(3); - if (mimeTypeArg == null) { - throw new NullPointerException("mimeTypeArg unexpectedly null."); - } String encodingArg = (String) args.get(4); - if (encodingArg == null) { - throw new NullPointerException("encodingArg unexpectedly null."); - } String historyUrlArg = (String) args.get(5); - if (historyUrlArg == null) { - throw new NullPointerException("historyUrlArg unexpectedly null."); - } api.loadDataWithBaseUrl( - instanceIdArg.longValue(), + (instanceIdArg == null) ? null : instanceIdArg.longValue(), baseUrlArg, dataArg, mimeTypeArg, @@ -489,7 +599,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (headersArg == null) { throw new NullPointerException("headersArg unexpectedly null."); } - api.loadUrl(instanceIdArg.longValue(), urlArg, headersArg); + api.loadUrl( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + urlArg, + headersArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -522,7 +635,8 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (dataArg == null) { throw new NullPointerException("dataArg unexpectedly null."); } - api.postUrl(instanceIdArg.longValue(), urlArg, dataArg); + api.postUrl( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), urlArg, dataArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -547,7 +661,8 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - String output = api.getUrl(instanceIdArg.longValue()); + String output = + api.getUrl((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", output); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -572,7 +687,8 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - Boolean output = api.canGoBack(instanceIdArg.longValue()); + Boolean output = + api.canGoBack((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", output); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -597,7 +713,8 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - Boolean output = api.canGoForward(instanceIdArg.longValue()); + Boolean output = + api.canGoForward((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", output); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -622,7 +739,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - api.goBack(instanceIdArg.longValue()); + api.goBack((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -647,7 +764,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - api.goForward(instanceIdArg.longValue()); + api.goForward((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -672,7 +789,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - api.reload(instanceIdArg.longValue()); + api.reload((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -701,7 +818,9 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (includeDiskFilesArg == null) { throw new NullPointerException("includeDiskFilesArg unexpectedly null."); } - api.clearCache(instanceIdArg.longValue(), includeDiskFilesArg); + api.clearCache( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + includeDiskFilesArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -746,7 +865,9 @@ public void error(Throwable error) { }; api.evaluateJavascript( - instanceIdArg.longValue(), javascriptStringArg, resultCallback); + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + javascriptStringArg, + resultCallback); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); reply.reply(wrapped); @@ -770,7 +891,8 @@ public void error(Throwable error) { if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - String output = api.getTitle(instanceIdArg.longValue()); + String output = + api.getTitle((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", output); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -803,7 +925,10 @@ public void error(Throwable error) { if (yArg == null) { throw new NullPointerException("yArg unexpectedly null."); } - api.scrollTo(instanceIdArg.longValue(), xArg.longValue(), yArg.longValue()); + api.scrollTo( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (xArg == null) ? null : xArg.longValue(), + (yArg == null) ? null : yArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -836,7 +961,10 @@ public void error(Throwable error) { if (yArg == null) { throw new NullPointerException("yArg unexpectedly null."); } - api.scrollBy(instanceIdArg.longValue(), xArg.longValue(), yArg.longValue()); + api.scrollBy( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (xArg == null) ? null : xArg.longValue(), + (yArg == null) ? null : yArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -861,7 +989,8 @@ public void error(Throwable error) { if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - Long output = api.getScrollX(instanceIdArg.longValue()); + Long output = + api.getScrollX((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", output); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -886,7 +1015,8 @@ public void error(Throwable error) { if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - Long output = api.getScrollY(instanceIdArg.longValue()); + Long output = + api.getScrollY((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", output); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -943,7 +1073,10 @@ public void error(Throwable error) { throw new NullPointerException("webViewClientInstanceIdArg unexpectedly null."); } api.setWebViewClient( - instanceIdArg.longValue(), webViewClientInstanceIdArg.longValue()); + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (webViewClientInstanceIdArg == null) + ? null + : webViewClientInstanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -976,7 +1109,10 @@ public void error(Throwable error) { "javaScriptChannelInstanceIdArg unexpectedly null."); } api.addJavaScriptChannel( - instanceIdArg.longValue(), javaScriptChannelInstanceIdArg.longValue()); + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (javaScriptChannelInstanceIdArg == null) + ? null + : javaScriptChannelInstanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1009,7 +1145,10 @@ public void error(Throwable error) { "javaScriptChannelInstanceIdArg unexpectedly null."); } api.removeJavaScriptChannel( - instanceIdArg.longValue(), javaScriptChannelInstanceIdArg.longValue()); + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (javaScriptChannelInstanceIdArg == null) + ? null + : javaScriptChannelInstanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1037,11 +1176,9 @@ public void error(Throwable error) { throw new NullPointerException("instanceIdArg unexpectedly null."); } Number listenerInstanceIdArg = (Number) args.get(1); - if (listenerInstanceIdArg == null) { - throw new NullPointerException("listenerInstanceIdArg unexpectedly null."); - } api.setDownloadListener( - instanceIdArg.longValue(), listenerInstanceIdArg.longValue()); + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (listenerInstanceIdArg == null) ? null : listenerInstanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1069,11 +1206,9 @@ public void error(Throwable error) { throw new NullPointerException("instanceIdArg unexpectedly null."); } Number clientInstanceIdArg = (Number) args.get(1); - if (clientInstanceIdArg == null) { - throw new NullPointerException("clientInstanceIdArg unexpectedly null."); - } api.setWebChromeClient( - instanceIdArg.longValue(), clientInstanceIdArg.longValue()); + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (clientInstanceIdArg == null) ? null : clientInstanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1104,7 +1239,9 @@ public void error(Throwable error) { if (colorArg == null) { throw new NullPointerException("colorArg unexpectedly null."); } - api.setBackgroundColor(instanceIdArg.longValue(), colorArg.longValue()); + api.setBackgroundColor( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (colorArg == null) ? null : colorArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1126,33 +1263,33 @@ private WebSettingsHostApiCodec() {} /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebSettingsHostApi { - void create(Long instanceId, Long webViewInstanceId); + void create(@NonNull Long instanceId, @NonNull Long webViewInstanceId); - void dispose(Long instanceId); + void dispose(@NonNull Long instanceId); - void setDomStorageEnabled(Long instanceId, Boolean flag); + void setDomStorageEnabled(@NonNull Long instanceId, @NonNull Boolean flag); - void setJavaScriptCanOpenWindowsAutomatically(Long instanceId, Boolean flag); + void setJavaScriptCanOpenWindowsAutomatically(@NonNull Long instanceId, @NonNull Boolean flag); - void setSupportMultipleWindows(Long instanceId, Boolean support); + void setSupportMultipleWindows(@NonNull Long instanceId, @NonNull Boolean support); - void setJavaScriptEnabled(Long instanceId, Boolean flag); + void setJavaScriptEnabled(@NonNull Long instanceId, @NonNull Boolean flag); - void setUserAgentString(Long instanceId, String userAgentString); + void setUserAgentString(@NonNull Long instanceId, @Nullable String userAgentString); - void setMediaPlaybackRequiresUserGesture(Long instanceId, Boolean require); + void setMediaPlaybackRequiresUserGesture(@NonNull Long instanceId, @NonNull Boolean require); - void setSupportZoom(Long instanceId, Boolean support); + void setSupportZoom(@NonNull Long instanceId, @NonNull Boolean support); - void setLoadWithOverviewMode(Long instanceId, Boolean overview); + void setLoadWithOverviewMode(@NonNull Long instanceId, @NonNull Boolean overview); - void setUseWideViewPort(Long instanceId, Boolean use); + void setUseWideViewPort(@NonNull Long instanceId, @NonNull Boolean use); - void setDisplayZoomControls(Long instanceId, Boolean enabled); + void setDisplayZoomControls(@NonNull Long instanceId, @NonNull Boolean enabled); - void setBuiltInZoomControls(Long instanceId, Boolean enabled); + void setBuiltInZoomControls(@NonNull Long instanceId, @NonNull Boolean enabled); - void setAllowFileAccess(Long instanceId, Boolean enabled); + void setAllowFileAccess(@NonNull Long instanceId, @NonNull Boolean enabled); /** The codec used by WebSettingsHostApi. */ static MessageCodec getCodec() { @@ -1181,7 +1318,9 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (webViewInstanceIdArg == null) { throw new NullPointerException("webViewInstanceIdArg unexpectedly null."); } - api.create(instanceIdArg.longValue(), webViewInstanceIdArg.longValue()); + api.create( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (webViewInstanceIdArg == null) ? null : webViewInstanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1206,7 +1345,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - api.dispose(instanceIdArg.longValue()); + api.dispose((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1237,7 +1376,8 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (flagArg == null) { throw new NullPointerException("flagArg unexpectedly null."); } - api.setDomStorageEnabled(instanceIdArg.longValue(), flagArg); + api.setDomStorageEnabled( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), flagArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1268,7 +1408,8 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (flagArg == null) { throw new NullPointerException("flagArg unexpectedly null."); } - api.setJavaScriptCanOpenWindowsAutomatically(instanceIdArg.longValue(), flagArg); + api.setJavaScriptCanOpenWindowsAutomatically( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), flagArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1299,7 +1440,8 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (supportArg == null) { throw new NullPointerException("supportArg unexpectedly null."); } - api.setSupportMultipleWindows(instanceIdArg.longValue(), supportArg); + api.setSupportMultipleWindows( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), supportArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1330,7 +1472,8 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (flagArg == null) { throw new NullPointerException("flagArg unexpectedly null."); } - api.setJavaScriptEnabled(instanceIdArg.longValue(), flagArg); + api.setJavaScriptEnabled( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), flagArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1358,10 +1501,9 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { throw new NullPointerException("instanceIdArg unexpectedly null."); } String userAgentStringArg = (String) args.get(1); - if (userAgentStringArg == null) { - throw new NullPointerException("userAgentStringArg unexpectedly null."); - } - api.setUserAgentString(instanceIdArg.longValue(), userAgentStringArg); + api.setUserAgentString( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + userAgentStringArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1392,7 +1534,8 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (requireArg == null) { throw new NullPointerException("requireArg unexpectedly null."); } - api.setMediaPlaybackRequiresUserGesture(instanceIdArg.longValue(), requireArg); + api.setMediaPlaybackRequiresUserGesture( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), requireArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1423,7 +1566,8 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (supportArg == null) { throw new NullPointerException("supportArg unexpectedly null."); } - api.setSupportZoom(instanceIdArg.longValue(), supportArg); + api.setSupportZoom( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), supportArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1454,7 +1598,8 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (overviewArg == null) { throw new NullPointerException("overviewArg unexpectedly null."); } - api.setLoadWithOverviewMode(instanceIdArg.longValue(), overviewArg); + api.setLoadWithOverviewMode( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), overviewArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1485,7 +1630,8 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (useArg == null) { throw new NullPointerException("useArg unexpectedly null."); } - api.setUseWideViewPort(instanceIdArg.longValue(), useArg); + api.setUseWideViewPort( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), useArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1516,7 +1662,8 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (enabledArg == null) { throw new NullPointerException("enabledArg unexpectedly null."); } - api.setDisplayZoomControls(instanceIdArg.longValue(), enabledArg); + api.setDisplayZoomControls( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), enabledArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1547,7 +1694,8 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (enabledArg == null) { throw new NullPointerException("enabledArg unexpectedly null."); } - api.setBuiltInZoomControls(instanceIdArg.longValue(), enabledArg); + api.setBuiltInZoomControls( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), enabledArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1578,7 +1726,8 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (enabledArg == null) { throw new NullPointerException("enabledArg unexpectedly null."); } - api.setAllowFileAccess(instanceIdArg.longValue(), enabledArg); + api.setAllowFileAccess( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), enabledArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1601,7 +1750,7 @@ private JavaScriptChannelHostApiCodec() {} /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface JavaScriptChannelHostApi { - void create(Long instanceId, String channelName); + void create(@NonNull Long instanceId, @NonNull String channelName); /** The codec used by JavaScriptChannelHostApi. */ static MessageCodec getCodec() { @@ -1631,7 +1780,8 @@ static void setup(BinaryMessenger binaryMessenger, JavaScriptChannelHostApi api) if (channelNameArg == null) { throw new NullPointerException("channelNameArg unexpectedly null."); } - api.create(instanceIdArg.longValue(), channelNameArg); + api.create( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), channelNameArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1668,7 +1818,7 @@ static MessageCodec getCodec() { return JavaScriptChannelFlutterApiCodec.INSTANCE; } - public void dispose(Long instanceIdArg, Reply callback) { + public void dispose(@NonNull Long instanceIdArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, @@ -1681,7 +1831,8 @@ public void dispose(Long instanceIdArg, Reply callback) { }); } - public void postMessage(Long instanceIdArg, String messageArg, Reply callback) { + public void postMessage( + @NonNull Long instanceIdArg, @NonNull String messageArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, @@ -1703,7 +1854,7 @@ private WebViewClientHostApiCodec() {} /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebViewClientHostApi { - void create(Long instanceId, Boolean shouldOverrideUrlLoading); + void create(@NonNull Long instanceId, @NonNull Boolean shouldOverrideUrlLoading); /** The codec used by WebViewClientHostApi. */ static MessageCodec getCodec() { @@ -1734,7 +1885,9 @@ static void setup(BinaryMessenger binaryMessenger, WebViewClientHostApi api) { throw new NullPointerException( "shouldOverrideUrlLoadingArg unexpectedly null."); } - api.create(instanceIdArg.longValue(), shouldOverrideUrlLoadingArg); + api.create( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + shouldOverrideUrlLoadingArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1797,7 +1950,7 @@ static MessageCodec getCodec() { return WebViewClientFlutterApiCodec.INSTANCE; } - public void dispose(Long instanceIdArg, Reply callback) { + public void dispose(@NonNull Long instanceIdArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.WebViewClientFlutterApi.dispose", getCodec()); @@ -1809,7 +1962,10 @@ public void dispose(Long instanceIdArg, Reply callback) { } public void onPageStarted( - Long instanceIdArg, Long webViewInstanceIdArg, String urlArg, Reply callback) { + @NonNull Long instanceIdArg, + @NonNull Long webViewInstanceIdArg, + @NonNull String urlArg, + Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, @@ -1823,7 +1979,10 @@ public void onPageStarted( } public void onPageFinished( - Long instanceIdArg, Long webViewInstanceIdArg, String urlArg, Reply callback) { + @NonNull Long instanceIdArg, + @NonNull Long webViewInstanceIdArg, + @NonNull String urlArg, + Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, @@ -1837,10 +1996,10 @@ public void onPageFinished( } public void onReceivedRequestError( - Long instanceIdArg, - Long webViewInstanceIdArg, - WebResourceRequestData requestArg, - WebResourceErrorData errorArg, + @NonNull Long instanceIdArg, + @NonNull Long webViewInstanceIdArg, + @NonNull WebResourceRequestData requestArg, + @NonNull WebResourceErrorData errorArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -1856,11 +2015,11 @@ public void onReceivedRequestError( } public void onReceivedError( - Long instanceIdArg, - Long webViewInstanceIdArg, - Long errorCodeArg, - String descriptionArg, - String failingUrlArg, + @NonNull Long instanceIdArg, + @NonNull Long webViewInstanceIdArg, + @NonNull Long errorCodeArg, + @NonNull String descriptionArg, + @NonNull String failingUrlArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -1881,9 +2040,9 @@ public void onReceivedError( } public void requestLoading( - Long instanceIdArg, - Long webViewInstanceIdArg, - WebResourceRequestData requestArg, + @NonNull Long instanceIdArg, + @NonNull Long webViewInstanceIdArg, + @NonNull WebResourceRequestData requestArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -1898,7 +2057,10 @@ public void requestLoading( } public void urlLoading( - Long instanceIdArg, Long webViewInstanceIdArg, String urlArg, Reply callback) { + @NonNull Long instanceIdArg, + @NonNull Long webViewInstanceIdArg, + @NonNull String urlArg, + Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading", getCodec()); @@ -1918,7 +2080,7 @@ private DownloadListenerHostApiCodec() {} /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface DownloadListenerHostApi { - void create(Long instanceId); + void create(@NonNull Long instanceId); /** The codec used by DownloadListenerHostApi. */ static MessageCodec getCodec() { @@ -1944,7 +2106,7 @@ static void setup(BinaryMessenger binaryMessenger, DownloadListenerHostApi api) if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - api.create(instanceIdArg.longValue()); + api.create((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -1981,7 +2143,7 @@ static MessageCodec getCodec() { return DownloadListenerFlutterApiCodec.INSTANCE; } - public void dispose(Long instanceIdArg, Reply callback) { + public void dispose(@NonNull Long instanceIdArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.DownloadListenerFlutterApi.dispose", getCodec()); @@ -1993,12 +2155,12 @@ public void dispose(Long instanceIdArg, Reply callback) { } public void onDownloadStart( - Long instanceIdArg, - String urlArg, - String userAgentArg, - String contentDispositionArg, - String mimetypeArg, - Long contentLengthArg, + @NonNull Long instanceIdArg, + @NonNull String urlArg, + @NonNull String userAgentArg, + @NonNull String contentDispositionArg, + @NonNull String mimetypeArg, + @NonNull Long contentLengthArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -2028,7 +2190,7 @@ private WebChromeClientHostApiCodec() {} /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebChromeClientHostApi { - void create(Long instanceId, Long webViewClientInstanceId); + void create(@NonNull Long instanceId, @NonNull Long webViewClientInstanceId); /** The codec used by WebChromeClientHostApi. */ static MessageCodec getCodec() { @@ -2058,7 +2220,11 @@ static void setup(BinaryMessenger binaryMessenger, WebChromeClientHostApi api) { if (webViewClientInstanceIdArg == null) { throw new NullPointerException("webViewClientInstanceIdArg unexpectedly null."); } - api.create(instanceIdArg.longValue(), webViewClientInstanceIdArg.longValue()); + api.create( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (webViewClientInstanceIdArg == null) + ? null + : webViewClientInstanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -2081,9 +2247,11 @@ private FlutterAssetManagerHostApiCodec() {} /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface FlutterAssetManagerHostApi { - List list(String path); + @NonNull + List list(@NonNull String path); - String getAssetFilePathByName(String name); + @NonNull + String getAssetFilePathByName(@NonNull String name); /** The codec used by FlutterAssetManagerHostApi. */ static MessageCodec getCodec() { @@ -2173,7 +2341,7 @@ static MessageCodec getCodec() { return WebChromeClientFlutterApiCodec.INSTANCE; } - public void dispose(Long instanceIdArg, Reply callback) { + public void dispose(@NonNull Long instanceIdArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.WebChromeClientFlutterApi.dispose", getCodec()); @@ -2185,7 +2353,10 @@ public void dispose(Long instanceIdArg, Reply callback) { } public void onProgressChanged( - Long instanceIdArg, Long webViewInstanceIdArg, Long progressArg, Reply callback) { + @NonNull Long instanceIdArg, + @NonNull Long webViewInstanceIdArg, + @NonNull Long progressArg, + Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, @@ -2199,11 +2370,87 @@ public void onProgressChanged( } } + private static class WebStorageHostApiCodec extends StandardMessageCodec { + public static final WebStorageHostApiCodec INSTANCE = new WebStorageHostApiCodec(); + + private WebStorageHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface WebStorageHostApi { + void create(@NonNull Long instanceId); + + void deleteAllData(@NonNull Long instanceId); + + /** The codec used by WebStorageHostApi. */ + static MessageCodec getCodec() { + return WebStorageHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `WebStorageHostApi` to handle messages through the `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, WebStorageHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebStorageHostApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + api.create((instanceIdArg == null) ? null : instanceIdArg.longValue()); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebStorageHostApi.deleteAllData", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + api.deleteAllData((instanceIdArg == null) ? null : instanceIdArg.longValue()); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + private static Map wrapError(Throwable exception) { Map errorMap = new HashMap<>(); errorMap.put("message", exception.toString()); errorMap.put("code", exception.getClass().getSimpleName()); - errorMap.put("details", null); + errorMap.put( + "details", + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); return errorMap; } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebStorageHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebStorageHostApiImpl.java new file mode 100644 index 000000000000..42e7603c0279 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebStorageHostApiImpl.java @@ -0,0 +1,53 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.WebStorage; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebStorageHostApi; + +/** + * Host api implementation for {@link WebStorage}. + * + *

Handles creating {@link WebStorage}s that intercommunicate with a paired Dart object. + */ +public class WebStorageHostApiImpl implements WebStorageHostApi { + private final InstanceManager instanceManager; + private final WebStorageCreator webStorageCreator; + + /** Handles creating {@link WebStorage} for a {@link WebStorageHostApiImpl}. */ + public static class WebStorageCreator { + /** + * Creates a {@link WebStorage}. + * + * @return the created {@link WebStorage}. Defaults to {@link WebStorage#getInstance} + */ + public WebStorage createWebStorage() { + return WebStorage.getInstance(); + } + } + + /** + * Creates a host API that handles creating {@link WebStorage} and invoke its methods. + * + * @param instanceManager maintains instances stored to communicate with Dart objects + * @param webStorageCreator handles creating {@link WebStorage}s + */ + public WebStorageHostApiImpl( + InstanceManager instanceManager, WebStorageCreator webStorageCreator) { + this.instanceManager = instanceManager; + this.webStorageCreator = webStorageCreator; + } + + @Override + public void create(Long instanceId) { + instanceManager.addInstance(webStorageCreator.createWebStorage(), instanceId); + } + + @Override + public void deleteAllData(Long instanceId) { + final WebStorage webStorage = (WebStorage) instanceManager.getInstance(instanceId); + webStorage.deleteAllData(); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java index 9e462faa58a7..b4885688f7ac 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java @@ -14,6 +14,7 @@ import androidx.webkit.WebResourceErrorCompat; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewClientFlutterApi; +import java.util.HashMap; /** * Flutter Api implementation for {@link WebViewClient}. @@ -26,40 +27,39 @@ public class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { @RequiresApi(api = Build.VERSION_CODES.M) static GeneratedAndroidWebView.WebResourceErrorData createWebResourceErrorData( WebResourceError error) { - final GeneratedAndroidWebView.WebResourceErrorData errorData = - new GeneratedAndroidWebView.WebResourceErrorData(); - errorData.setErrorCode((long) error.getErrorCode()); - errorData.setDescription(error.getDescription().toString()); - - return errorData; + return new GeneratedAndroidWebView.WebResourceErrorData.Builder() + .setErrorCode((long) error.getErrorCode()) + .setDescription(error.getDescription().toString()) + .build(); } @SuppressLint("RequiresFeature") static GeneratedAndroidWebView.WebResourceErrorData createWebResourceErrorData( WebResourceErrorCompat error) { - final GeneratedAndroidWebView.WebResourceErrorData errorData = - new GeneratedAndroidWebView.WebResourceErrorData(); - errorData.setErrorCode((long) error.getErrorCode()); - errorData.setDescription(error.getDescription().toString()); - - return errorData; + return new GeneratedAndroidWebView.WebResourceErrorData.Builder() + .setErrorCode((long) error.getErrorCode()) + .setDescription(error.getDescription().toString()) + .build(); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) static GeneratedAndroidWebView.WebResourceRequestData createWebResourceRequestData( WebResourceRequest request) { - final GeneratedAndroidWebView.WebResourceRequestData requestData = - new GeneratedAndroidWebView.WebResourceRequestData(); - requestData.setUrl(request.getUrl().toString()); - requestData.setIsForMainFrame(request.isForMainFrame()); + final GeneratedAndroidWebView.WebResourceRequestData.Builder requestData = + new GeneratedAndroidWebView.WebResourceRequestData.Builder() + .setUrl(request.getUrl().toString()) + .setIsForMainFrame(request.isForMainFrame()) + .setHasGesture(request.hasGesture()) + .setMethod(request.getMethod()) + .setRequestHeaders( + request.getRequestHeaders() != null + ? request.getRequestHeaders() + : new HashMap<>()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { requestData.setIsRedirect(request.isRedirect()); } - requestData.setHasGesture(request.hasGesture()); - requestData.setMethod(request.getMethod()); - requestData.setRequestHeaders(request.getRequestHeaders()); - return requestData; + return requestData.build(); } /** diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java index 4ef622fe47cb..67202ebef16d 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java @@ -19,6 +19,7 @@ import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebSettingsHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebStorageHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewClientHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewHostApi; @@ -116,6 +117,9 @@ private void setUp( FlutterAssetManagerHostApi.setup( binaryMessenger, new FlutterAssetManagerHostApiImpl(flutterAssetManager)); CookieManagerHostApi.setup(binaryMessenger, new CookieManagerHostApiImpl()); + WebStorageHostApi.setup( + binaryMessenger, + new WebStorageHostApiImpl(instanceManager, new WebStorageHostApiImpl.WebStorageCreator())); } @Override diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java index 0f3161355dcb..afc3efee80ff 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java @@ -28,11 +28,6 @@ *

Handles creating {@link WebView}s that intercommunicate with a paired Dart object. */ public class WebViewHostApiImpl implements WebViewHostApi { - // TODO(bparrishMines): This can be removed once pigeon supports null values: https://github.com/flutter/flutter/issues/59118 - // Workaround to represent null Strings since pigeon doesn't support null - // values. - private static final String nullStringIdentifier = ""; - private final InstanceManager instanceManager; private final WebViewProxy webViewProxy; // Only used with WebView using virtual displays. @@ -355,8 +350,7 @@ public void dispose(Long instanceId) { @Override public void loadData(Long instanceId, String data, String mimeType, String encoding) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); - webView.loadData( - data, parseNullStringIdentifier(mimeType), parseNullStringIdentifier(encoding)); + webView.loadData(data, mimeType, encoding); } @Override @@ -368,12 +362,7 @@ public void loadDataWithBaseUrl( String encoding, String historyUrl) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); - webView.loadDataWithBaseURL( - parseNullStringIdentifier(baseUrl), - data, - parseNullStringIdentifier(mimeType), - parseNullStringIdentifier(encoding), - parseNullStringIdentifier(historyUrl)); + webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); } @Override @@ -391,8 +380,7 @@ public void postUrl(Long instanceId, String url, byte[] data) { @Override public String getUrl(Long instanceId) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); - final String result = webView.getUrl(); - return result != null ? result : nullStringIdentifier; + return webView.getUrl(); } @Override @@ -441,8 +429,7 @@ public void evaluateJavascript( @Override public String getTitle(Long instanceId) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); - final String result = webView.getTitle(); - return result != null ? result : nullStringIdentifier; + return webView.getTitle(); } @Override @@ -513,13 +500,4 @@ public void setBackgroundColor(Long instanceId, Long color) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); webView.setBackgroundColor(color.intValue()); } - - @Nullable - private static String parseNullStringIdentifier(String value) { - if (value.equals(nullStringIdentifier)) { - return null; - } - - return value; - } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebStorageHostApiImplTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebStorageHostApiImplTest.java new file mode 100644 index 000000000000..e2845c2842a9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebStorageHostApiImplTest.java @@ -0,0 +1,41 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.webkit.WebStorage; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class WebStorageHostApiImplTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public WebStorage mockWebStorage; + + @Mock WebStorageHostApiImpl.WebStorageCreator mockWebStorageCreator; + + InstanceManager testInstanceManager; + WebStorageHostApiImpl testHostApiImpl; + + @Before + public void setUp() { + testInstanceManager = new InstanceManager(); + when(mockWebStorageCreator.createWebStorage()).thenReturn(mockWebStorage); + testHostApiImpl = new WebStorageHostApiImpl(testInstanceManager, mockWebStorageCreator); + testHostApiImpl.create(0L); + } + + @Test + public void deleteAllData() { + testHostApiImpl.deleteAllData(0L); + verify(mockWebStorage).deleteAllData(); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java index 62d272366a6f..c2abd25c5a66 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java @@ -4,16 +4,22 @@ package io.flutter.plugins.webviewflutter; +import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.net.Uri; +import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebViewClient; import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.WebViewClientCompatImpl; import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.WebViewClientCreator; +import java.util.HashMap; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -96,4 +102,20 @@ public void urlLoading() { webViewClient.shouldOverrideUrlLoading(mockWebView, ""); verify(mockFlutterApi, never()).urlLoading((WebViewClient) any(), any(), any(), any()); } + + @Test + public void convertWebResourceRequestWithNullHeaders() { + final Uri mockUri = mock(Uri.class); + when(mockUri.toString()).thenReturn(""); + + final WebResourceRequest mockRequest = mock(WebResourceRequest.class); + when(mockRequest.getMethod()).thenReturn("method"); + when(mockRequest.getUrl()).thenReturn(mockUri); + when(mockRequest.isForMainFrame()).thenReturn(true); + when(mockRequest.getRequestHeaders()).thenReturn(null); + + final GeneratedAndroidWebView.WebResourceRequestData data = + WebViewClientFlutterApiImpl.createWebResourceRequestData(mockRequest); + assertEquals(data.getRequestHeaders(), new HashMap()); + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java index 2312b764342f..5be39ab963a3 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java @@ -166,8 +166,7 @@ public void loadData() { @Test public void loadDataWithNullValues() { - testHostApiImpl.loadData( - 0L, "VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", "", ""); + testHostApiImpl.loadData(0L, "VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", null, null); verify(mockWebView).loadData("VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", null, null); } @@ -192,12 +191,7 @@ public void loadDataWithBaseUrl() { @Test public void loadDataWithBaseUrlAndNullValues() { testHostApiImpl.loadDataWithBaseUrl( - 0L, - "", - "VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", - "", - "", - ""); + 0L, null, "VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", null, null, null); verify(mockWebView) .loadDataWithBaseURL(null, "VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", null, null, null); } diff --git a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart index 58f2f369bcf5..51e09912da23 100644 --- a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart @@ -14,7 +14,6 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:webview_flutter_android/webview_android.dart'; @@ -1377,6 +1376,50 @@ Future main() async { ); }, ); + + testWidgets( + 'clearCache should clear local storage', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer onPageFinished = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (_) => onPageFinished.complete(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await onPageFinished.future; + + await controller.runJavascript('localStorage.setItem("myCat", "Tom");'); + + expect( + controller.runJavascriptReturningResult( + 'localStorage.getItem("myCat");', + ), + completion('"Tom"'), + ); + + await controller.clearCache(); + + expect( + controller.runJavascriptReturningResult( + 'localStorage.getItem("myCat");', + ), + completion('null'), + ); + }, + ); } // JavaScript booleans evaluate to different string values on Android and iOS. diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart index 0d0cd59af796..5d19ca71ac84 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -8,7 +8,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_driver/driver_extension.dart'; import 'package:path_provider/path_provider.dart'; @@ -110,37 +109,33 @@ class _WebViewExampleState extends State<_WebViewExample> { _SampleMenu(_controller.future), ], ), - // We're using a Builder here so we have a context that is below the Scaffold - // to allow calling Scaffold.of(context) so we can show a snackbar. - body: Builder(builder: (BuildContext context) { - return WebView( - initialUrl: 'https://flutter.dev', - onWebViewCreated: (WebViewController controller) { - _controller.complete(controller); - }, - onProgress: (int progress) { - print('WebView is loading (progress : $progress%)'); - }, - navigationDelegate: (NavigationRequest request) { - if (request.url.startsWith('https://www.youtube.com/')) { - print('blocking navigation to $request}'); - return NavigationDecision.prevent; - } - print('allowing navigation to $request'); - return NavigationDecision.navigate; - }, - onPageStarted: (String url) { - print('Page started loading: $url'); - }, - onPageFinished: (String url) { - print('Page finished loading: $url'); - }, - javascriptChannels: _createJavascriptChannels(context), - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent', - backgroundColor: const Color(0x80000000), - ); - }), + body: WebView( + initialUrl: 'https://flutter.dev', + onWebViewCreated: (WebViewController controller) { + _controller.complete(controller); + }, + onProgress: (int progress) { + print('WebView is loading (progress : $progress%)'); + }, + navigationDelegate: (NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $request}'); + return NavigationDecision.prevent; + } + print('allowing navigation to $request'); + return NavigationDecision.navigate; + }, + onPageStarted: (String url) { + print('Page started loading: $url'); + }, + onPageFinished: (String url) { + print('Page finished loading: $url'); + }, + javascriptChannels: _createJavascriptChannels(context), + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent', + backgroundColor: const Color(0x80000000), + ), floatingActionButton: favoriteButton(), ); } @@ -154,8 +149,7 @@ class _WebViewExampleState extends State<_WebViewExample> { return FloatingActionButton( onPressed: () async { final String url = (await controller.data!.currentUrl())!; - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Favorited $url')), ); }, @@ -323,8 +317,7 @@ class _SampleMenu extends StatelessWidget { WebViewController controller, BuildContext context) async { final String cookies = await controller.runJavascriptReturningResult('document.cookie'); - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(SnackBar( + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Column( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, @@ -340,8 +333,7 @@ class _SampleMenu extends StatelessWidget { WebViewController controller, BuildContext context) async { await controller.runJavascript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(const SnackBar( + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Added a test entry to cache.'), )); } @@ -356,8 +348,7 @@ class _SampleMenu extends StatelessWidget { Future _onClearCache( WebViewController controller, BuildContext context) async { await controller.clearCache(); - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(const SnackBar( + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Cache cleared.'), )); } @@ -369,8 +360,7 @@ class _SampleMenu extends StatelessWidget { if (!hadCookies) { message = 'There are no cookies.'; } - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(SnackBar( + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(message), )); } @@ -474,8 +464,7 @@ class _NavigationControls extends StatelessWidget { if (await controller!.canGoBack()) { await controller.goBack(); } else { - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('No back history item')), ); return; @@ -490,8 +479,7 @@ class _NavigationControls extends StatelessWidget { if (await controller!.canGoForward()) { await controller.goForward(); } else { - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('No forward history item')), ); diff --git a/packages/webview_flutter/webview_flutter_android/generatePigeons.sh b/packages/webview_flutter/webview_flutter_android/generatePigeons.sh deleted file mode 100755 index 30a6918fc922..000000000000 --- a/packages/webview_flutter/webview_flutter_android/generatePigeons.sh +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright 2013 The Flutter Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -flutter pub run pigeon \ ---input pigeons/android_webview.dart \ ---dart_out lib/src/android_webview.pigeon.dart \ ---dart_test_out test/android_webview.pigeon.dart \ ---java_out android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.Java \ ---java_package io.flutter.plugins.webviewflutter diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart index 10989321a9bb..f858fb39a943 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart @@ -11,11 +11,6 @@ import 'package:flutter/widgets.dart' show AndroidViewSurface; import 'android_webview.pigeon.dart'; import 'android_webview_api_impls.dart'; -// TODO(bparrishMines): This can be removed once pigeon supports null values: https://github.com/flutter/flutter/issues/59118 -// Workaround to represent null Strings since pigeon doesn't support null -// values. -const String _nullStringIdentifier = ''; - /// An Android View that displays web pages. /// /// **Basic usage** @@ -102,8 +97,8 @@ class WebView { return api.loadDataFromInstance( this, data, - mimeType ?? _nullStringIdentifier, - encoding ?? _nullStringIdentifier, + mimeType, + encoding, ); } @@ -151,11 +146,11 @@ class WebView { }) { return api.loadDataWithBaseUrlFromInstance( this, - baseUrl ?? _nullStringIdentifier, + baseUrl, data, - mimeType ?? _nullStringIdentifier, - encoding ?? _nullStringIdentifier, - historyUrl ?? _nullStringIdentifier, + mimeType, + encoding, + historyUrl, ); } @@ -184,12 +179,8 @@ class WebView { /// begun, the current page may not have changed. /// /// Returns null if no page has been loaded. - Future getUrl() async { - final String result = await api.getUrlFromInstance(this); - if (result == _nullStringIdentifier) { - return null; - } - return result; + Future getUrl() { + return api.getUrlFromInstance(this); } /// Whether this WebView has a back history item. @@ -235,27 +226,19 @@ class WebView { /// JavaScript state from an empty WebView is no longer persisted across /// navigations like [loadUrl]. For example, global variables and functions /// defined before calling [loadUrl]) will not exist in the loaded page. - Future evaluateJavascript(String javascriptString) async { - final String result = await api.evaluateJavascriptFromInstance( + Future evaluateJavascript(String javascriptString) { + return api.evaluateJavascriptFromInstance( this, javascriptString, ); - if (result == _nullStringIdentifier) { - return null; - } - return result; } // TODO(bparrishMines): Update documentation when WebViewClient.onReceivedTitle is added. /// Gets the title for the current page. /// /// Returns null if no page has been loaded. - Future getTitle() async { - final String result = await api.getTitleFromInstance(this); - if (result == _nullStringIdentifier) { - return null; - } - return result; + Future getTitle() { + return api.getTitleFromInstance(this); } // TODO(bparrishMines): Update documentation when onScrollChanged is added. @@ -337,9 +320,11 @@ class WebView { /// Registers the interface to be used when content can not be handled by the rendering engine, and should be downloaded instead. /// /// This will replace the current handler. - Future setDownloadListener(DownloadListener listener) { - DownloadListener.api.createFromInstance(listener); - return api.setDownloadListenerFromInstance(this, listener); + Future setDownloadListener(DownloadListener? listener) async { + await Future.wait(>[ + if (listener != null) DownloadListener.api.createFromInstance(listener), + api.setDownloadListenerFromInstance(this, listener) + ]); } /// Sets the chrome handler. @@ -347,7 +332,7 @@ class WebView { /// This is an implementation of [WebChromeClient] for use in handling /// JavaScript dialogs, favicons, titles, and the progress. This will replace /// the current handler. - Future setWebChromeClient(WebChromeClient client) { + Future setWebChromeClient(WebChromeClient? client) async { // WebView requires a WebViewClient because of a bug fix that makes // calls to WebViewClient.requestLoading/WebViewClient.urlLoading when a new // window is opened. This is to make sure a url opened by `Window.open` has @@ -356,8 +341,11 @@ class WebView { _currentWebViewClient != null, "Can't set a WebChromeClient without setting a WebViewClient first.", ); - WebChromeClient.api.createFromInstance(client, _currentWebViewClient!); - return api.setWebChromeClientFromInstance(this, client); + await Future.wait(>[ + if (client != null) + WebChromeClient.api.createFromInstance(client, _currentWebViewClient!), + api.setWebChromeClientFromInstance(this, client), + ]); } /// Sets the background color of this WebView. @@ -477,7 +465,7 @@ class WebSettings { /// If the string is empty, the system default value will be used. Note that /// starting from KITKAT Android version, changing the user-agent while /// loading a web page causes WebView to initiate loading once again. - Future setUserAgentString(String userAgentString) { + Future setUserAgentString(String? userAgentString) { return api.setUserAgentStringFromInstance(this, userAgentString); } @@ -852,3 +840,30 @@ class FlutterAssetManager { Future getAssetFilePathByName(String name) => api.getAssetFilePathByName(name); } + +/// Manages the JavaScript storage APIs provided by the [WebView]. +/// +/// Wraps [WebStorage](https://developer.android.com/reference/android/webkit/WebStorage). +class WebStorage { + /// Constructs a [WebStorage]. + /// + /// This constructor is only used for testing. An instance should be obtained + /// with [WebStorage.instance]. + @visibleForTesting + WebStorage() { + AndroidWebViewFlutterApis.instance.ensureSetUp(); + api.createFromInstance(this); + } + + /// Pigeon Host Api implementation for [WebStorage]. + @visibleForTesting + static WebStorageHostApiImpl api = WebStorageHostApiImpl(); + + /// The singleton instance of this class. + static WebStorage instance = WebStorage(); + + /// Clears all storage currently being used by the JavaScript storage APIs. + Future deleteAllData() { + return api.deleteAllDataFromInstance(this); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart index 20391c43d966..4491e162ce9c 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart @@ -1,8 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - -// Autogenerated from Pigeon (v1.0.9), do not edit directly. +// Autogenerated from Pigeon (v3.0.3), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name // @dart = 2.12 @@ -13,12 +12,21 @@ import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; import 'package:flutter/services.dart'; class WebResourceRequestData { - String? url; - bool? isForMainFrame; + WebResourceRequestData({ + required this.url, + required this.isForMainFrame, + this.isRedirect, + required this.hasGesture, + required this.method, + required this.requestHeaders, + }); + + String url; + bool isForMainFrame; bool? isRedirect; - bool? hasGesture; - String? method; - Map? requestHeaders; + bool hasGesture; + String method; + Map requestHeaders; Object encode() { final Map pigeonMap = {}; @@ -33,20 +41,26 @@ class WebResourceRequestData { static WebResourceRequestData decode(Object message) { final Map pigeonMap = message as Map; - return WebResourceRequestData() - ..url = pigeonMap['url'] as String? - ..isForMainFrame = pigeonMap['isForMainFrame'] as bool? - ..isRedirect = pigeonMap['isRedirect'] as bool? - ..hasGesture = pigeonMap['hasGesture'] as bool? - ..method = pigeonMap['method'] as String? - ..requestHeaders = (pigeonMap['requestHeaders'] as Map?) - ?.cast(); + return WebResourceRequestData( + url: pigeonMap['url']! as String, + isForMainFrame: pigeonMap['isForMainFrame']! as bool, + isRedirect: pigeonMap['isRedirect'] as bool?, + hasGesture: pigeonMap['hasGesture']! as bool, + method: pigeonMap['method']! as String, + requestHeaders: (pigeonMap['requestHeaders'] as Map?)! + .cast(), + ); } } class WebResourceErrorData { - int? errorCode; - String? description; + WebResourceErrorData({ + required this.errorCode, + required this.description, + }); + + int errorCode; + String description; Object encode() { final Map pigeonMap = {}; @@ -57,9 +71,10 @@ class WebResourceErrorData { static WebResourceErrorData decode(Object message) { final Map pigeonMap = message as Map; - return WebResourceErrorData() - ..errorCode = pigeonMap['errorCode'] as int? - ..description = pigeonMap['description'] as String?; + return WebResourceErrorData( + errorCode: pigeonMap['errorCode']! as int, + description: pigeonMap['description']! as String, + ); } } @@ -88,7 +103,6 @@ class CookieManagerHostApi { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -98,6 +112,11 @@ class CookieManagerHostApi { message: error['message'] as String?, details: error['details'], ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); } else { return (replyMap['result'] as bool?)!; } @@ -108,12 +127,11 @@ class CookieManagerHostApi { 'dev.flutter.pigeon.CookieManagerHostApi.setCookie', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_url, arg_value]) as Map?; + .send([arg_url, arg_value]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -149,13 +167,12 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.create', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_useHybridComposition]) + await channel.send([arg_instanceId, arg_useHybridComposition]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -175,12 +192,11 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.dispose', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -196,18 +212,17 @@ class WebViewHostApi { } Future loadData(int arg_instanceId, String arg_data, - String arg_mimeType, String arg_encoding) async { + String? arg_mimeType, String? arg_encoding) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.loadData', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel.send( - [arg_instanceId, arg_data, arg_mimeType, arg_encoding]) + [arg_instanceId, arg_data, arg_mimeType, arg_encoding]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -224,15 +239,15 @@ class WebViewHostApi { Future loadDataWithBaseUrl( int arg_instanceId, - String arg_baseUrl, + String? arg_baseUrl, String arg_data, - String arg_mimeType, - String arg_encoding, - String arg_historyUrl) async { + String? arg_mimeType, + String? arg_encoding, + String? arg_historyUrl) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel.send([ + final Map? replyMap = await channel.send([ arg_instanceId, arg_baseUrl, arg_data, @@ -244,7 +259,6 @@ class WebViewHostApi { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -265,13 +279,12 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.loadUrl', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_url, arg_headers]) + await channel.send([arg_instanceId, arg_url, arg_headers]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -292,13 +305,12 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.postUrl', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_url, arg_data]) + await channel.send([arg_instanceId, arg_url, arg_data]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -313,17 +325,16 @@ class WebViewHostApi { } } - Future getUrl(int arg_instanceId) async { + Future getUrl(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.getUrl', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -334,7 +345,7 @@ class WebViewHostApi { details: error['details'], ); } else { - return (replyMap['result'] as String?)!; + return (replyMap['result'] as String?); } } @@ -343,12 +354,11 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.canGoBack', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -358,6 +368,11 @@ class WebViewHostApi { message: error['message'] as String?, details: error['details'], ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); } else { return (replyMap['result'] as bool?)!; } @@ -368,12 +383,11 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.canGoForward', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -383,6 +397,11 @@ class WebViewHostApi { message: error['message'] as String?, details: error['details'], ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); } else { return (replyMap['result'] as bool?)!; } @@ -393,12 +412,11 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.goBack', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -418,12 +436,11 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.goForward', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -443,12 +460,11 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.reload', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -468,13 +484,12 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.clearCache', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_includeDiskFiles]) + await channel.send([arg_instanceId, arg_includeDiskFiles]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -489,19 +504,18 @@ class WebViewHostApi { } } - Future evaluateJavascript( + Future evaluateJavascript( int arg_instanceId, String arg_javascriptString) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.evaluateJavascript', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_javascriptString]) + await channel.send([arg_instanceId, arg_javascriptString]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -512,21 +526,20 @@ class WebViewHostApi { details: error['details'], ); } else { - return (replyMap['result'] as String?)!; + return (replyMap['result'] as String?); } } - Future getTitle(int arg_instanceId) async { + Future getTitle(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.getTitle', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -537,7 +550,7 @@ class WebViewHostApi { details: error['details'], ); } else { - return (replyMap['result'] as String?)!; + return (replyMap['result'] as String?); } } @@ -545,13 +558,13 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.scrollTo', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_x, arg_y]) as Map?; + final Map? replyMap = + await channel.send([arg_instanceId, arg_x, arg_y]) + as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -570,13 +583,13 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.scrollBy', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_x, arg_y]) as Map?; + final Map? replyMap = + await channel.send([arg_instanceId, arg_x, arg_y]) + as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -596,12 +609,11 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.getScrollX', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -611,6 +623,11 @@ class WebViewHostApi { message: error['message'] as String?, details: error['details'], ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); } else { return (replyMap['result'] as int?)!; } @@ -621,12 +638,11 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.getScrollY', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -636,6 +652,11 @@ class WebViewHostApi { message: error['message'] as String?, details: error['details'], ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); } else { return (replyMap['result'] as int?)!; } @@ -647,12 +668,11 @@ class WebViewHostApi { codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_enabled]) as Map?; + await channel.send([arg_enabled]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -673,13 +693,12 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.setWebViewClient', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_instanceId, arg_webViewClientInstanceId]) + .send([arg_instanceId, arg_webViewClientInstanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -700,13 +719,12 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_instanceId, arg_javaScriptChannelInstanceId]) + .send([arg_instanceId, arg_javaScriptChannelInstanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -727,13 +745,12 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_instanceId, arg_javaScriptChannelInstanceId]) + .send([arg_instanceId, arg_javaScriptChannelInstanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -749,18 +766,17 @@ class WebViewHostApi { } Future setDownloadListener( - int arg_instanceId, int arg_listenerInstanceId) async { + int arg_instanceId, int? arg_listenerInstanceId) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.setDownloadListener', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_listenerInstanceId]) + await channel.send([arg_instanceId, arg_listenerInstanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -776,18 +792,17 @@ class WebViewHostApi { } Future setWebChromeClient( - int arg_instanceId, int arg_clientInstanceId) async { + int arg_instanceId, int? arg_clientInstanceId) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.setWebChromeClient', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_clientInstanceId]) + await channel.send([arg_instanceId, arg_clientInstanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -807,12 +822,11 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.setBackgroundColor', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_instanceId, arg_color]) as Map?; + .send([arg_instanceId, arg_color]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -848,13 +862,12 @@ class WebSettingsHostApi { 'dev.flutter.pigeon.WebSettingsHostApi.create', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_webViewInstanceId]) + await channel.send([arg_instanceId, arg_webViewInstanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -874,12 +887,11 @@ class WebSettingsHostApi { 'dev.flutter.pigeon.WebSettingsHostApi.dispose', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -899,12 +911,11 @@ class WebSettingsHostApi { 'dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_instanceId, arg_flag]) as Map?; + .send([arg_instanceId, arg_flag]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -926,12 +937,11 @@ class WebSettingsHostApi { codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_instanceId, arg_flag]) as Map?; + .send([arg_instanceId, arg_flag]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -953,12 +963,11 @@ class WebSettingsHostApi { codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_instanceId, arg_support]) as Map?; + .send([arg_instanceId, arg_support]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -978,12 +987,11 @@ class WebSettingsHostApi { 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_instanceId, arg_flag]) as Map?; + .send([arg_instanceId, arg_flag]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -999,18 +1007,17 @@ class WebSettingsHostApi { } Future setUserAgentString( - int arg_instanceId, String arg_userAgentString) async { + int arg_instanceId, String? arg_userAgentString) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_userAgentString]) + await channel.send([arg_instanceId, arg_userAgentString]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -1032,12 +1039,11 @@ class WebSettingsHostApi { codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_instanceId, arg_require]) as Map?; + .send([arg_instanceId, arg_require]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -1057,12 +1063,11 @@ class WebSettingsHostApi { 'dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_instanceId, arg_support]) as Map?; + .send([arg_instanceId, arg_support]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -1082,13 +1087,13 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_overview]) as Map?; + final Map? replyMap = + await channel.send([arg_instanceId, arg_overview]) + as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -1108,12 +1113,11 @@ class WebSettingsHostApi { 'dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_instanceId, arg_use]) as Map?; + .send([arg_instanceId, arg_use]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -1134,12 +1138,11 @@ class WebSettingsHostApi { 'dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_instanceId, arg_enabled]) as Map?; + .send([arg_instanceId, arg_enabled]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -1160,12 +1163,11 @@ class WebSettingsHostApi { 'dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_instanceId, arg_enabled]) as Map?; + .send([arg_instanceId, arg_enabled]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -1185,12 +1187,11 @@ class WebSettingsHostApi { 'dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_instanceId, arg_enabled]) as Map?; + .send([arg_instanceId, arg_enabled]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -1226,13 +1227,12 @@ class JavaScriptChannelHostApi { 'dev.flutter.pigeon.JavaScriptChannelHostApi.create', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_channelName]) + await channel.send([arg_instanceId, arg_channelName]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -1325,13 +1325,12 @@ class WebViewClientHostApi { 'dev.flutter.pigeon.WebViewClientHostApi.create', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_instanceId, arg_shouldOverrideUrlLoading]) + .send([arg_instanceId, arg_shouldOverrideUrlLoading]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -1600,12 +1599,11 @@ class DownloadListenerHostApi { 'dev.flutter.pigeon.DownloadListenerHostApi.create', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -1712,13 +1710,12 @@ class WebChromeClientHostApi { 'dev.flutter.pigeon.WebChromeClientHostApi.create', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel - .send([arg_instanceId, arg_webViewClientInstanceId]) + .send([arg_instanceId, arg_webViewClientInstanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -1754,12 +1751,11 @@ class FlutterAssetManagerHostApi { 'dev.flutter.pigeon.FlutterAssetManagerHostApi.list', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_path]) as Map?; + await channel.send([arg_path]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -1769,6 +1765,11 @@ class FlutterAssetManagerHostApi { message: error['message'] as String?, details: error['details'], ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); } else { return (replyMap['result'] as List?)!.cast(); } @@ -1780,12 +1781,11 @@ class FlutterAssetManagerHostApi { codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_name]) as Map?; + await channel.send([arg_name]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -1795,6 +1795,11 @@ class FlutterAssetManagerHostApi { message: error['message'] as String?, details: error['details'], ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); } else { return (replyMap['result'] as String?)!; } @@ -1860,3 +1865,67 @@ abstract class WebChromeClientFlutterApi { } } } + +class _WebStorageHostApiCodec extends StandardMessageCodec { + const _WebStorageHostApiCodec(); +} + +class WebStorageHostApi { + /// Constructor for [WebStorageHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + WebStorageHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WebStorageHostApiCodec(); + + Future create(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebStorageHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future deleteAllData(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebStorageHostApi.deleteAllData', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart index ead60f6a2b35..b40a0518dca0 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart @@ -13,21 +13,20 @@ import 'instance_manager.dart'; /// Converts [WebResourceRequestData] to [WebResourceRequest] WebResourceRequest _toWebResourceRequest(WebResourceRequestData data) { return WebResourceRequest( - url: data.url!, - isForMainFrame: data.isForMainFrame!, + url: data.url, + isForMainFrame: data.isForMainFrame, isRedirect: data.isRedirect, - hasGesture: data.hasGesture!, - method: data.method!, - requestHeaders: - data.requestHeaders?.cast() ?? {}, + hasGesture: data.hasGesture, + method: data.method, + requestHeaders: data.requestHeaders.cast(), ); } /// Converts [WebResourceErrorData] to [WebResourceError]. WebResourceError _toWebResourceError(WebResourceErrorData data) { return WebResourceError( - errorCode: data.errorCode!, - description: data.description!, + errorCode: data.errorCode, + description: data.description, ); } @@ -115,8 +114,8 @@ class WebViewHostApiImpl extends WebViewHostApi { Future loadDataFromInstance( WebView instance, String data, - String mimeType, - String encoding, + String? mimeType, + String? encoding, ) { return loadData( instanceManager.getInstanceId(instance)!, @@ -129,11 +128,11 @@ class WebViewHostApiImpl extends WebViewHostApi { /// Helper method to convert instances ids to objects. Future loadDataWithBaseUrlFromInstance( WebView instance, - String baseUrl, + String? baseUrl, String data, - String mimeType, - String encoding, - String historyUrl, + String? mimeType, + String? encoding, + String? historyUrl, ) { return loadDataWithBaseUrl( instanceManager.getInstanceId(instance)!, @@ -164,7 +163,7 @@ class WebViewHostApiImpl extends WebViewHostApi { } /// Helper method to convert instances ids to objects. - Future getUrlFromInstance(WebView instance) { + Future getUrlFromInstance(WebView instance) { return getUrl(instanceManager.getInstanceId(instance)!); } @@ -202,16 +201,18 @@ class WebViewHostApiImpl extends WebViewHostApi { } /// Helper method to convert instances ids to objects. - Future evaluateJavascriptFromInstance( + Future evaluateJavascriptFromInstance( WebView instance, String javascriptString, ) { return evaluateJavascript( - instanceManager.getInstanceId(instance)!, javascriptString); + instanceManager.getInstanceId(instance)!, + javascriptString, + ); } /// Helper method to convert instances ids to objects. - Future getTitleFromInstance(WebView instance) { + Future getTitleFromInstance(WebView instance) { return getTitle(instanceManager.getInstanceId(instance)!); } @@ -271,22 +272,22 @@ class WebViewHostApiImpl extends WebViewHostApi { /// Helper method to convert instances ids to objects. Future setDownloadListenerFromInstance( WebView instance, - DownloadListener listener, + DownloadListener? listener, ) { return setDownloadListener( instanceManager.getInstanceId(instance)!, - instanceManager.getInstanceId(listener)!, + listener != null ? instanceManager.getInstanceId(listener) : null, ); } /// Helper method to convert instances ids to objects. Future setWebChromeClientFromInstance( WebView instance, - WebChromeClient client, + WebChromeClient? client, ) { return setWebChromeClient( instanceManager.getInstanceId(instance)!, - instanceManager.getInstanceId(client)!, + client != null ? instanceManager.getInstanceId(client) : null, ); } @@ -370,7 +371,7 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { /// Helper method to convert instances ids to objects. Future setUserAgentStringFromInstance( WebSettings instance, - String userAgentString, + String? userAgentString, ) { return setUserAgentString( instanceManager.getInstanceId(instance)!, @@ -787,3 +788,30 @@ class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { instance!.onProgressChanged(webViewInstance!, progress); } } + +/// Host api implementation for [WebStorage]. +class WebStorageHostApiImpl extends WebStorageHostApi { + /// Constructs a [WebStorageHostApiImpl]. + WebStorageHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : super(binaryMessenger: binaryMessenger) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with java objects. + late final InstanceManager instanceManager; + + /// Helper method to convert instances ids to objects. + Future createFromInstance(WebStorage instance) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + return create(instanceId); + } + } + + /// Helper method to convert instances ids to objects. + Future deleteAllDataFromInstance(WebStorage instance) { + return deleteAllData(instanceManager.getInstanceId(instance)!); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart index 7200aaa4a322..28d169c9cb94 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart @@ -23,6 +23,7 @@ class WebViewAndroidWidget extends StatefulWidget { @visibleForTesting this.webViewProxy = const WebViewProxy(), @visibleForTesting this.flutterAssetManager = const android_webview.FlutterAssetManager(), + @visibleForTesting this.webStorage, }); /// Initial parameters used to setup the WebView. @@ -59,6 +60,9 @@ class WebViewAndroidWidget extends StatefulWidget { final Widget Function(WebViewAndroidPlatformController controller) onBuildWidget; + /// Manages the JavaScript storage APIs. + final android_webview.WebStorage? webStorage; + @override State createState() => _WebViewAndroidWidgetState(); } @@ -76,6 +80,7 @@ class _WebViewAndroidWidgetState extends State { javascriptChannelRegistry: widget.javascriptChannelRegistry, webViewProxy: widget.webViewProxy, flutterAssetManager: widget.flutterAssetManager, + webStorage: widget.webStorage, ); } @@ -102,7 +107,9 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { @visibleForTesting this.webViewProxy = const WebViewProxy(), @visibleForTesting this.flutterAssetManager = const android_webview.FlutterAssetManager(), - }) : assert(creationParams.webSettings?.hasNavigationDelegate != null), + @visibleForTesting android_webview.WebStorage? webStorage, + }) : webStorage = webStorage ?? android_webview.WebStorage.instance, + assert(creationParams.webSettings?.hasNavigationDelegate != null), super(callbacksHandler) { webView = webViewProxy.createWebView( useHybridComposition: useHybridComposition, @@ -160,6 +167,9 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { late final WebViewAndroidWebChromeClient webChromeClient = WebViewAndroidWebChromeClient(); + /// Manages the JavaScript storage APIs. + final android_webview.WebStorage webStorage; + /// Receive various notifications and requests for [android_webview.WebView]. @visibleForTesting WebViewAndroidWebViewClient get webViewClient => _webViewClient; @@ -254,7 +264,10 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { Future reload() => webView.reload(); @override - Future clearCache() => webView.clearCache(true); + Future clearCache() { + webView.clearCache(true); + return webStorage.deleteAllData(); + } @override Future updateSettings(WebSettings setting) async { diff --git a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart index b29835266717..70ecd99d3638 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -4,18 +4,51 @@ import 'package:pigeon/pigeon.dart'; +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/android_webview.pigeon.dart', + dartTestOut: 'test/test_android_webview.pigeon.dart', + dartOptions: DartOptions(copyrightHeader: [ + 'Copyright 2013 The Flutter Authors. All rights reserved.', + 'Use of this source code is governed by a BSD-style license that can be', + 'found in the LICENSE file.', + ]), + javaOut: + 'android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java', + javaOptions: JavaOptions( + package: 'io.flutter.plugins.webviewflutter', + className: 'GeneratedAndroidWebView', + copyrightHeader: [ + 'Copyright 2013 The Flutter Authors. All rights reserved.', + 'Use of this source code is governed by a BSD-style license that can be', + 'found in the LICENSE file.', + ], + ), + ), +) class WebResourceRequestData { - String? url; - bool? isForMainFrame; + WebResourceRequestData( + this.url, + this.isForMainFrame, + this.isRedirect, + this.hasGesture, + this.method, + this.requestHeaders, + ); + + String url; + bool isForMainFrame; bool? isRedirect; - bool? hasGesture; - String? method; - Map? requestHeaders; + bool hasGesture; + String method; + Map requestHeaders; } class WebResourceErrorData { - int? errorCode; - String? description; + WebResourceErrorData(this.errorCode, this.description); + + int errorCode; + String description; } @HostApi() @@ -35,17 +68,17 @@ abstract class WebViewHostApi { void loadData( int instanceId, String data, - String mimeType, - String encoding, + String? mimeType, + String? encoding, ); void loadDataWithBaseUrl( int instanceId, - String baseUrl, + String? baseUrl, String data, - String mimeType, - String encoding, - String historyUrl, + String? mimeType, + String? encoding, + String? historyUrl, ); void loadUrl( @@ -60,7 +93,7 @@ abstract class WebViewHostApi { Uint8List data, ); - String getUrl(int instanceId); + String? getUrl(int instanceId); bool canGoBack(int instanceId); @@ -75,12 +108,12 @@ abstract class WebViewHostApi { void clearCache(int instanceId, bool includeDiskFiles); @async - String evaluateJavascript( + String? evaluateJavascript( int instanceId, String javascriptString, ); - String getTitle(int instanceId); + String? getTitle(int instanceId); void scrollTo(int instanceId, int x, int y); @@ -98,9 +131,9 @@ abstract class WebViewHostApi { void removeJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId); - void setDownloadListener(int instanceId, int listenerInstanceId); + void setDownloadListener(int instanceId, int? listenerInstanceId); - void setWebChromeClient(int instanceId, int clientInstanceId); + void setWebChromeClient(int instanceId, int? clientInstanceId); void setBackgroundColor(int instanceId, int color); } @@ -119,7 +152,7 @@ abstract class WebSettingsHostApi { void setJavaScriptEnabled(int instanceId, bool flag); - void setUserAgentString(int instanceId, String userAgentString); + void setUserAgentString(int instanceId, String? userAgentString); void setMediaPlaybackRequiresUserGesture(int instanceId, bool require); @@ -222,3 +255,10 @@ abstract class WebChromeClientFlutterApi { void onProgressChanged(int instanceId, int webViewInstanceId, int progress); } + +@HostApi(dartHostTestHandler: 'TestWebStorageHostApi') +abstract class WebStorageHostApi { + void create(int instanceId); + + void deleteAllData(int instanceId); +} diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 35f0e2f2a5a0..9a7c48a4dcd8 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.8.3 +version: 2.8.6 environment: sdk: ">=2.14.0 <3.0.0" @@ -29,4 +29,4 @@ dev_dependencies: sdk: flutter mockito: ^5.1.0 pedantic: ^1.10.0 - pigeon: 1.0.9 + pigeon: ^3.0.3 diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart index 91385ff2d364..4c63ab025702 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart @@ -10,8 +10,8 @@ import 'package:webview_flutter_android/src/android_webview.pigeon.dart'; import 'package:webview_flutter_android/src/android_webview_api_impls.dart'; import 'package:webview_flutter_android/src/instance_manager.dart'; -import 'android_webview.pigeon.dart'; import 'android_webview_test.mocks.dart'; +import 'test_android_webview.pigeon.dart'; @GenerateMocks([ CookieManagerHostApi, @@ -21,6 +21,7 @@ import 'android_webview_test.mocks.dart'; TestJavaScriptChannelHostApi, TestWebChromeClientHostApi, TestWebSettingsHostApi, + TestWebStorageHostApi, TestWebViewClientHostApi, TestWebViewHostApi, TestAssetManagerHostApi, @@ -84,8 +85,8 @@ void main() { verify(mockPlatformHostApi.loadData( webViewInstanceId, 'hello', - '', - '', + null, + null, )); }); @@ -112,11 +113,11 @@ void main() { webView.loadDataWithBaseUrl(data: 'hello'); verify(mockPlatformHostApi.loadDataWithBaseUrl( webViewInstanceId, - '', + null, 'hello', - '', - '', - '', + null, + null, + null, )); }); @@ -526,14 +527,15 @@ void main() { flutterApi.onReceivedRequestError( mockWebViewClientInstanceId, mockWebViewInstanceId, - WebResourceRequestData() - ..url = 'https://www.google.com' - ..isForMainFrame = true - ..hasGesture = true - ..method = 'POST', - WebResourceErrorData() - ..errorCode = 34 - ..description = 'error description', + WebResourceRequestData( + url: 'https://www.google.com', + isForMainFrame: true, + hasGesture: true, + method: 'POST', + isRedirect: false, + requestHeaders: {}, + ), + WebResourceErrorData(errorCode: 34, description: 'error description'), ); verify(mockWebViewClient.onReceivedRequestError( @@ -564,11 +566,14 @@ void main() { flutterApi.requestLoading( mockWebViewClientInstanceId, mockWebViewInstanceId, - WebResourceRequestData() - ..url = 'https://www.google.com' - ..isForMainFrame = true - ..hasGesture = true - ..method = 'POST', + WebResourceRequestData( + url: 'https://www.google.com', + isForMainFrame: true, + hasGesture: true, + method: 'POST', + isRedirect: true, + requestHeaders: {}, + ), ); verify(mockWebViewClient.requestLoading( @@ -677,4 +682,29 @@ void main() { verify(CookieManager.api.clearCookies()); }); }); + + group('WebStorage', () { + late MockTestWebStorageHostApi mockPlatformHostApi; + + late WebStorage webStorage; + late int webStorageInstanceId; + + setUp(() { + mockPlatformHostApi = MockTestWebStorageHostApi(); + TestWebStorageHostApi.setup(mockPlatformHostApi); + + webStorage = WebStorage(); + webStorageInstanceId = + WebStorage.api.instanceManager.getInstanceId(webStorage)!; + }); + + test('create', () { + verify(mockPlatformHostApi.create(webStorageInstanceId)); + }); + + test('deleteAllData', () { + webStorage.deleteAllData(); + verify(mockPlatformHostApi.deleteAllData(webStorageInstanceId)); + }); + }); } diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart index d6023d7de3aa..85ab6685ca34 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart @@ -10,7 +10,7 @@ import 'package:mockito/mockito.dart' as _i1; import 'package:webview_flutter_android/src/android_webview.dart' as _i2; import 'package:webview_flutter_android/src/android_webview.pigeon.dart' as _i3; -import 'android_webview.pigeon.dart' as _i5; +import 'test_android_webview.pigeon.dart' as _i5; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -201,6 +201,25 @@ class MockTestWebSettingsHostApi extends _i1.Mock returnValueForMissingStub: null); } +/// A class which mocks [TestWebStorageHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWebStorageHostApi extends _i1.Mock + implements _i5.TestWebStorageHostApi { + MockTestWebStorageHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId) => + super.noSuchMethod(Invocation.method(#create, [instanceId]), + returnValueForMissingStub: null); + @override + void deleteAllData(int? instanceId) => + super.noSuchMethod(Invocation.method(#deleteAllData, [instanceId]), + returnValueForMissingStub: null); +} + /// A class which mocks [TestWebViewClientHostApi]. /// /// See the documentation for Mockito's code generation for more information. @@ -258,9 +277,8 @@ class MockTestWebViewHostApi extends _i1.Mock super.noSuchMethod(Invocation.method(#postUrl, [instanceId, url, data]), returnValueForMissingStub: null); @override - String getUrl(int? instanceId) => - (super.noSuchMethod(Invocation.method(#getUrl, [instanceId]), - returnValue: '') as String); + String? getUrl(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getUrl, [instanceId])) as String?); @override bool canGoBack(int? instanceId) => (super.noSuchMethod(Invocation.method(#canGoBack, [instanceId]), @@ -287,16 +305,16 @@ class MockTestWebViewHostApi extends _i1.Mock Invocation.method(#clearCache, [instanceId, includeDiskFiles]), returnValueForMissingStub: null); @override - _i4.Future evaluateJavascript( + _i4.Future evaluateJavascript( int? instanceId, String? javascriptString) => (super.noSuchMethod( Invocation.method( #evaluateJavascript, [instanceId, javascriptString]), - returnValue: Future.value('')) as _i4.Future); + returnValue: Future.value()) as _i4.Future); @override - String getTitle(int? instanceId) => - (super.noSuchMethod(Invocation.method(#getTitle, [instanceId]), - returnValue: '') as String); + String? getTitle(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getTitle, [instanceId])) + as String?); @override void scrollTo(int? instanceId, int? x, int? y) => super.noSuchMethod(Invocation.method(#scrollTo, [instanceId, x, y]), diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.pigeon.dart similarity index 94% rename from packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart rename to packages/webview_flutter/webview_flutter_android/test/test_android_webview.pigeon.dart index 746126346750..e3c5909f3207 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.pigeon.dart @@ -1,10 +1,10 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - -// Autogenerated from Pigeon (v1.0.9), do not edit directly. +// Autogenerated from Pigeon (v3.0.3), do not edit directly. // See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis +// ignore_for_file: avoid_relative_lib_imports // @dart = 2.12 import 'dart:async'; import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; @@ -23,20 +23,21 @@ abstract class TestWebViewHostApi { void create(int instanceId, bool useHybridComposition); void dispose(int instanceId); - void loadData(int instanceId, String data, String mimeType, String encoding); - void loadDataWithBaseUrl(int instanceId, String baseUrl, String data, - String mimeType, String encoding, String historyUrl); + void loadData( + int instanceId, String data, String? mimeType, String? encoding); + void loadDataWithBaseUrl(int instanceId, String? baseUrl, String data, + String? mimeType, String? encoding, String? historyUrl); void loadUrl(int instanceId, String url, Map headers); void postUrl(int instanceId, String url, Uint8List data); - String getUrl(int instanceId); + String? getUrl(int instanceId); bool canGoBack(int instanceId); bool canGoForward(int instanceId); void goBack(int instanceId); void goForward(int instanceId); void reload(int instanceId); void clearCache(int instanceId, bool includeDiskFiles); - Future evaluateJavascript(int instanceId, String javascriptString); - String getTitle(int instanceId); + Future evaluateJavascript(int instanceId, String javascriptString); + String? getTitle(int instanceId); void scrollTo(int instanceId, int x, int y); void scrollBy(int instanceId, int x, int y); int getScrollX(int instanceId); @@ -45,8 +46,8 @@ abstract class TestWebViewHostApi { void setWebViewClient(int instanceId, int webViewClientInstanceId); void addJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId); void removeJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId); - void setDownloadListener(int instanceId, int listenerInstanceId); - void setWebChromeClient(int instanceId, int clientInstanceId); + void setDownloadListener(int instanceId, int? listenerInstanceId); + void setWebChromeClient(int instanceId, int? clientInstanceId); void setBackgroundColor(int instanceId, int color); static void setup(TestWebViewHostApi? api, {BinaryMessenger? binaryMessenger}) { @@ -109,13 +110,8 @@ abstract class TestWebViewHostApi { assert(arg_data != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadData was null, expected non-null String.'); final String? arg_mimeType = (args[2] as String?); - assert(arg_mimeType != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadData was null, expected non-null String.'); final String? arg_encoding = (args[3] as String?); - assert(arg_encoding != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadData was null, expected non-null String.'); - api.loadData( - arg_instanceId!, arg_data!, arg_mimeType!, arg_encoding!); + api.loadData(arg_instanceId!, arg_data!, arg_mimeType, arg_encoding); return {}; }); } @@ -135,22 +131,14 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl was null, expected non-null int.'); final String? arg_baseUrl = (args[1] as String?); - assert(arg_baseUrl != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl was null, expected non-null String.'); final String? arg_data = (args[2] as String?); assert(arg_data != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl was null, expected non-null String.'); final String? arg_mimeType = (args[3] as String?); - assert(arg_mimeType != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl was null, expected non-null String.'); final String? arg_encoding = (args[4] as String?); - assert(arg_encoding != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl was null, expected non-null String.'); final String? arg_historyUrl = (args[5] as String?); - assert(arg_historyUrl != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl was null, expected non-null String.'); - api.loadDataWithBaseUrl(arg_instanceId!, arg_baseUrl!, arg_data!, - arg_mimeType!, arg_encoding!, arg_historyUrl!); + api.loadDataWithBaseUrl(arg_instanceId!, arg_baseUrl, arg_data!, + arg_mimeType, arg_encoding, arg_historyUrl); return {}; }); } @@ -220,7 +208,7 @@ abstract class TestWebViewHostApi { final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getUrl was null, expected non-null int.'); - final String output = api.getUrl(arg_instanceId!); + final String? output = api.getUrl(arg_instanceId!); return {'result': output}; }); } @@ -359,7 +347,7 @@ abstract class TestWebViewHostApi { final String? arg_javascriptString = (args[1] as String?); assert(arg_javascriptString != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.evaluateJavascript was null, expected non-null String.'); - final String output = await api.evaluateJavascript( + final String? output = await api.evaluateJavascript( arg_instanceId!, arg_javascriptString!); return {'result': output}; }); @@ -379,7 +367,7 @@ abstract class TestWebViewHostApi { final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getTitle was null, expected non-null int.'); - final String output = api.getTitle(arg_instanceId!); + final String? output = api.getTitle(arg_instanceId!); return {'result': output}; }); } @@ -575,9 +563,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setDownloadListener was null, expected non-null int.'); final int? arg_listenerInstanceId = (args[1] as int?); - assert(arg_listenerInstanceId != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.setDownloadListener was null, expected non-null int.'); - api.setDownloadListener(arg_instanceId!, arg_listenerInstanceId!); + api.setDownloadListener(arg_instanceId!, arg_listenerInstanceId); return {}; }); } @@ -597,9 +583,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebChromeClient was null, expected non-null int.'); final int? arg_clientInstanceId = (args[1] as int?); - assert(arg_clientInstanceId != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebChromeClient was null, expected non-null int.'); - api.setWebChromeClient(arg_instanceId!, arg_clientInstanceId!); + api.setWebChromeClient(arg_instanceId!, arg_clientInstanceId); return {}; }); } @@ -642,7 +626,7 @@ abstract class TestWebSettingsHostApi { void setJavaScriptCanOpenWindowsAutomatically(int instanceId, bool flag); void setSupportMultipleWindows(int instanceId, bool support); void setJavaScriptEnabled(int instanceId, bool flag); - void setUserAgentString(int instanceId, String userAgentString); + void setUserAgentString(int instanceId, String? userAgentString); void setMediaPlaybackRequiresUserGesture(int instanceId, bool require); void setSupportZoom(int instanceId, bool support); void setLoadWithOverviewMode(int instanceId, bool overview); @@ -799,9 +783,7 @@ abstract class TestWebSettingsHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString was null, expected non-null int.'); final String? arg_userAgentString = (args[1] as String?); - assert(arg_userAgentString != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString was null, expected non-null String.'); - api.setUserAgentString(arg_instanceId!, arg_userAgentString!); + api.setUserAgentString(arg_instanceId!, arg_userAgentString); return {}; }); } @@ -1157,3 +1139,55 @@ abstract class TestAssetManagerHostApi { } } } + +class _TestWebStorageHostApiCodec extends StandardMessageCodec { + const _TestWebStorageHostApiCodec(); +} + +abstract class TestWebStorageHostApi { + static const MessageCodec codec = _TestWebStorageHostApiCodec(); + + void create(int instanceId); + void deleteAllData(int instanceId); + static void setup(TestWebStorageHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebStorageHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebStorageHostApi.create was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WebStorageHostApi.create was null, expected non-null int.'); + api.create(arg_instanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebStorageHostApi.deleteAllData', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebStorageHostApi.deleteAllData was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WebStorageHostApi.deleteAllData was null, expected non-null int.'); + api.deleteAllData(arg_instanceId!); + return {}; + }); + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart index af1093914047..a987f1cf548d 100644 --- a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart @@ -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/widgets.dart'; @@ -16,13 +15,14 @@ import 'package:webview_flutter_android/src/instance_manager.dart'; import 'package:webview_flutter_android/webview_android_widget.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; -import 'android_webview.pigeon.dart'; import 'android_webview_test.mocks.dart' show MockTestWebViewHostApi; +import 'test_android_webview.pigeon.dart'; import 'webview_android_widget_test.mocks.dart'; @GenerateMocks([ android_webview.FlutterAssetManager, android_webview.WebSettings, + android_webview.WebStorage, android_webview.WebView, WebViewAndroidDownloadListener, WebViewAndroidJavaScriptChannel, @@ -39,6 +39,7 @@ void main() { late MockFlutterAssetManager mockFlutterAssetManager; late MockWebView mockWebView; late MockWebSettings mockWebSettings; + late MockWebStorage mockWebStorage; late MockWebViewProxy mockWebViewProxy; late MockWebViewPlatformCallbacksHandler mockCallbacksHandler; @@ -54,6 +55,7 @@ void main() { mockFlutterAssetManager = MockFlutterAssetManager(); mockWebView = MockWebView(); mockWebSettings = MockWebSettings(); + mockWebStorage = MockWebStorage(); when(mockWebView.settings).thenReturn(mockWebSettings); mockWebViewProxy = MockWebViewProxy(); @@ -86,6 +88,7 @@ void main() { javascriptChannelRegistry: mockJavascriptChannelRegistry, webViewProxy: mockWebViewProxy, flutterAssetManager: mockFlutterAssetManager, + webStorage: mockWebStorage, onBuildWidget: (WebViewAndroidPlatformController controller) { testController = controller; return Container(); @@ -590,6 +593,7 @@ void main() { await testController.clearCache(); verify(mockWebView.clearCache(true)); + verify(mockWebStorage.deleteAllData()); }); testWidgets('evaluateJavascript', (WidgetTester tester) async { diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart index f4d9abbd2d3c..3385e7998ba9 100644 --- a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart @@ -121,6 +121,21 @@ class MockWebSettings extends _i1.Mock implements _i2.WebSettings { returnValueForMissingStub: Future.value()) as _i4.Future); } +/// A class which mocks [WebStorage]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebStorage extends _i1.Mock implements _i2.WebStorage { + MockWebStorage() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Future deleteAllData() => + (super.noSuchMethod(Invocation.method(#deleteAllData, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); +} + /// A class which mocks [WebView]. /// /// See the documentation for Mockito's code generation for more information. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md index 500bee7d2622..c7462ddd47d0 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Removes unnecessary imports. + +## 1.8.2 + +* Migrates from `ui.hash*` to `Object.hash*`. +* Updates minimum Flutter version to 2.5.0. + ## 1.8.1 * Update to use the `verify` method introduced in platform_plugin_interface 2.1.0. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart index bb2a4ac9fb50..f32881701cfb 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'package:flutter/services.dart'; -import '../platform_interface/javascript_channel_registry.dart'; import '../platform_interface/platform_interface.dart'; import '../types/types.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart index 6add51ce2d73..c1763cdae501 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart @@ -5,9 +5,6 @@ import 'package:flutter/widgets.dart'; import 'package:webview_flutter_platform_interface/src/types/types.dart'; -import 'auto_media_playback_policy.dart'; -import 'web_settings.dart'; - /// Configuration to use when creating a new [WebViewPlatformController]. /// /// The `autoMediaPlaybackPolicy` parameter must not be null. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart index 57c0a482089c..102ab10ccea7 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart @@ -62,7 +62,7 @@ class WebSetting { } @override - int get hashCode => hashValues(_value, isPresent); + int get hashCode => Object.hash(_value, isPresent); } /// Settings for configuring a WebViewPlatform. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml index 50b816d51d5d..f9e754931bb8 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutte issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview_flutter%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.8.1 +version: 1.8.2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.5.0" dependencies: flutter: diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart index 5d2c717be0e5..a34262fe460a 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart @@ -8,7 +8,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:webview_flutter_platform_interface/src/method_channel/webview_method_channel.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; void main() { diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart index df1b53090fc8..30795b01c83f 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart @@ -4,7 +4,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:webview_flutter_platform_interface/src/platform_interface/javascript_channel_registry.dart'; -import 'package:webview_flutter_platform_interface/src/types/javascript_channel.dart'; import 'package:webview_flutter_platform_interface/src/types/types.dart'; void main() { diff --git a/packages/webview_flutter/webview_flutter_web/CHANGELOG.md b/packages/webview_flutter/webview_flutter_web/CHANGELOG.md index ba8fc0fb01c9..9f7ebe368941 100644 --- a/packages/webview_flutter/webview_flutter_web/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_web/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +* Removes unnecessary imports. * Fixes unit tests to run on latest `master` version of Flutter. ## 0.1.0+1 diff --git a/packages/webview_flutter/webview_flutter_web/example/lib/main.dart b/packages/webview_flutter/webview_flutter_web/example/lib/main.dart index 9cf412d74e50..c183625be634 100644 --- a/packages/webview_flutter/webview_flutter_web/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_web/example/lib/main.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; diff --git a/packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart index 787b016d2b77..8cd74f660f58 100644 --- a/packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart @@ -4,7 +4,6 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter_web/webview_flutter_web.dart'; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index c3dd8726273e..f042dd081475 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,6 +1,15 @@ ## NEXT +* Removes unnecessary imports. + +## 2.7.3 + +* Removes two occurrences of the compiler warning: "'RequiresUserActionForMediaPlayback' is deprecated: first deprecated in ios 10.0". + +## 2.7.2 + * Fixes an integration test race condition. +* Migrates deprecated `Scaffold.showSnackBar` to `ScaffoldMessenger` in example app. ## 2.7.1 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart index 924a6caa0f85..ceff62e3d5e8 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart @@ -14,7 +14,6 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj index b681c4704ddd..c48b313daec2 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj @@ -11,6 +11,9 @@ 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */; }; 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 8FA6A87928062CD000A4B183 /* FWFInstanceManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */; }; + 8FB79B5328134C3100C101D3 /* FWFWebViewHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */; }; + 8FB79B55281B24F600C101D3 /* FWFDataConvertersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B54281B24F600C101D3 /* FWFDataConvertersTests.m */; }; 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 */; }; @@ -67,6 +70,9 @@ 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 = ""; }; + 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFInstanceManagerTests.m; sourceTree = ""; }; + 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFWebViewHostApiTests.m; sourceTree = ""; }; + 8FB79B54281B24F600C101D3 /* FWFDataConvertersTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFDataConvertersTests.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -126,6 +132,9 @@ 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */, 68BDCAED23C3F7CB00D9C032 /* Info.plist */, E43693B427512C0F00382F85 /* FLTCookieManagerTests.m */, + 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */, + 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */, + 8FB79B54281B24F600C101D3 /* FWFDataConvertersTests.m */, ); path = RunnerTests; sourceTree = ""; @@ -423,8 +432,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8FA6A87928062CD000A4B183 /* FWFInstanceManagerTests.m in Sources */, + 8FB79B5328134C3100C101D3 /* FWFWebViewHostApiTests.m in Sources */, 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */, 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.m in Sources */, + 8FB79B55281B24F600C101D3 /* FWFDataConvertersTests.m in Sources */, E43693B527512C0F00382F85 /* FLTCookieManagerTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m new file mode 100644 index 000000000000..55952373998d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +@interface FWFDataConvertersTests : XCTestCase +@end + +@implementation FWFDataConvertersTests +- (void)testFNSURLRequestFromRequestData { + NSURLRequest *request = FWFNSURLRequestFromRequestData([FWFNSUrlRequestData + makeWithUrl:@"https://flutter.dev" + httpMethod:@"post" + httpBody:[FlutterStandardTypedData typedDataWithBytes:[NSData data]] + allHttpHeaderFields:@{@"a" : @"header"}]); + + XCTAssertEqualObjects(request.URL, [NSURL URLWithString:@"https://flutter.dev"]); + XCTAssertEqualObjects(request.HTTPMethod, @"POST"); + XCTAssertEqualObjects(request.HTTPBody, [NSData data]); + XCTAssertEqualObjects(request.allHTTPHeaderFields, @{@"a" : @"header"}); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFInstanceManagerTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFInstanceManagerTests.m new file mode 100644 index 000000000000..7b40da131d23 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFInstanceManagerTests.m @@ -0,0 +1,40 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +@import webview_flutter_wkwebview; + +@interface FWFInstanceManagerTests : XCTestCase +@end + +@implementation FWFInstanceManagerTests +- (void)testAddInstance { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + NSObject *object = [[NSObject alloc] init]; + + [instanceManager addInstance:object withIdentifier:5]; + XCTAssertEqualObjects([instanceManager instanceForIdentifier:5], object); + XCTAssertEqual([instanceManager identifierForInstance:object], 5); +} + +- (void)testRemoveInstance { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + NSObject *object = [[NSObject alloc] init]; + [instanceManager addInstance:object withIdentifier:5]; + + [instanceManager removeInstance:object]; + XCTAssertNil([instanceManager instanceForIdentifier:5]); + XCTAssertEqual([instanceManager identifierForInstance:object], NSNotFound); +} + +- (void)testRemoveInstanceWithIdentifier { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + NSObject *object = [[NSObject alloc] init]; + [instanceManager addInstance:object withIdentifier:5]; + + [instanceManager removeInstanceWithIdentifier:5]; + XCTAssertNil([instanceManager instanceForIdentifier:5]); + XCTAssertEqual([instanceManager identifierForInstance:object], NSNotFound); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewHostApiTests.m new file mode 100644 index 000000000000..dc0c1ce593d2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewHostApiTests.m @@ -0,0 +1,340 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +#import + +@interface FWFWebViewHostApiTests : XCTestCase +@end + +@implementation FWFWebViewHostApiTests +- (void)testLoadRequest { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + FWFNSUrlRequestData *requestData = [FWFNSUrlRequestData makeWithUrl:@"https://www.flutter.dev" + httpMethod:@"get" + httpBody:nil + allHttpHeaderFields:@{@"a" : @"header"}]; + [hostApi loadRequestForWebViewWithIdentifier:@0 request:requestData error:&error]; + + NSURL *url = [NSURL URLWithString:@"https://www.flutter.dev"]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + request.HTTPMethod = @"get"; + request.allHTTPHeaderFields = @{@"a" : @"header"}; + OCMVerify([mockWebView loadRequest:request]); + XCTAssertNil(error); +} + +- (void)testLoadRequestWithInvalidUrl { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + OCMReject([mockWebView loadRequest:OCMOCK_ANY]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + FWFNSUrlRequestData *requestData = [FWFNSUrlRequestData makeWithUrl:@"%invalidUrl%" + httpMethod:nil + httpBody:nil + allHttpHeaderFields:@{}]; + [hostApi loadRequestForWebViewWithIdentifier:@0 request:requestData error:&error]; + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.code, @"FWFURLRequestParsingError"); + XCTAssertEqualObjects(error.message, @"Failed instantiating an NSURLRequest."); + XCTAssertEqualObjects(error.details, @"URL was: '%invalidUrl%'"); +} + +- (void)testSetCustomUserAgent { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostApi setUserAgentForWebViewWithIdentifier:@0 userAgent:@"userA" error:&error]; + OCMVerify([mockWebView setCustomUserAgent:@"userA"]); + XCTAssertNil(error); +} + +- (void)testURL { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + OCMStub([mockWebView URL]).andReturn([NSURL URLWithString:@"https://www.flutter.dev/"]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + XCTAssertEqualObjects([hostApi URLForWebViewWithIdentifier:@0 error:&error], + @"https://www.flutter.dev/"); + XCTAssertNil(error); +} + +- (void)testCanGoBack { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + OCMStub([mockWebView canGoBack]).andReturn(YES); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + XCTAssertEqualObjects([hostApi canGoBackForWebViewWithIdentifier:@0 error:&error], @YES); + XCTAssertNil(error); +} + +- (void)testSetUIDelegate { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + id mockDelegate = OCMProtocolMock(@protocol(WKUIDelegate)); + [instanceManager addInstance:mockDelegate withIdentifier:1]; + + FlutterError *error; + [hostApi setUIDelegateForWebViewWithIdentifier:@0 delegateIdentifier:@1 error:&error]; + OCMVerify([mockWebView setUIDelegate:mockDelegate]); + XCTAssertNil(error); +} + +- (void)testSetNavigationDelegate { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + id mockDelegate = OCMProtocolMock(@protocol(WKNavigationDelegate)); + [instanceManager addInstance:mockDelegate withIdentifier:1]; + FlutterError *error; + + [hostApi setNavigationDelegateForWebViewWithIdentifier:@0 delegateIdentifier:@1 error:&error]; + OCMVerify([mockWebView setNavigationDelegate:mockDelegate]); + XCTAssertNil(error); +} + +- (void)testEstimatedProgress { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + OCMStub([mockWebView estimatedProgress]).andReturn(34.0); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + XCTAssertEqualObjects([hostApi estimatedProgressForWebViewWithIdentifier:@0 error:&error], @34.0); + XCTAssertNil(error); +} + +- (void)testloadHTMLString { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostApi loadHTMLForWebViewWithIdentifier:@0 + HTMLString:@"myString" + baseURL:@"myBaseUrl" + error:&error]; + OCMVerify([mockWebView loadHTMLString:@"myString" baseURL:[NSURL URLWithString:@"myBaseUrl"]]); + XCTAssertNil(error); +} + +- (void)testLoadFileURL { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostApi loadFileForWebViewWithIdentifier:@0 + fileURL:@"myFolder/apple.txt" + readAccessURL:@"myFolder" + error:&error]; + XCTAssertNil(error); + OCMVerify([mockWebView loadFileURL:[NSURL fileURLWithPath:@"myFolder/apple.txt" isDirectory:NO] + allowingReadAccessToURL:[NSURL fileURLWithPath:@"myFolder/" isDirectory:YES] + + ]); +} + +- (void)testLoadFlutterAsset { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFAssetManager *mockAssetManager = OCMClassMock([FWFAssetManager class]); + OCMStub([mockAssetManager lookupKeyForAsset:@"assets/index.html"]) + .andReturn(@"myFolder/assets/index.html"); + + NSBundle *mockBundle = OCMClassMock([NSBundle class]); + OCMStub([mockBundle URLForResource:@"myFolder/assets/index" withExtension:@"html"]) + .andReturn([NSURL URLWithString:@"webview_flutter/myFolder/assets/index.html"]); + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager + bundle:mockBundle + assetManager:mockAssetManager]; + + FlutterError *error; + [hostApi loadAssetForWebViewWithIdentifier:@0 assetKey:@"assets/index.html" error:&error]; + + XCTAssertNil(error); + OCMVerify([mockWebView + loadFileURL:[NSURL URLWithString:@"webview_flutter/myFolder/assets/index.html"] + allowingReadAccessToURL:[NSURL URLWithString:@"webview_flutter/myFolder/assets/"]]); +} + +- (void)testCanGoForward { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + OCMStub([mockWebView canGoForward]).andReturn(NO); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + XCTAssertEqualObjects([hostApi canGoForwardForWebViewWithIdentifier:@0 error:&error], @NO); + XCTAssertNil(error); +} + +- (void)testGoBack { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostApi goBackForWebViewWithIdentifier:@0 error:&error]; + OCMVerify([mockWebView goBack]); + XCTAssertNil(error); +} + +- (void)testGoForward { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostApi goForwardForWebViewWithIdentifier:@0 error:&error]; + OCMVerify([mockWebView goForward]); + XCTAssertNil(error); +} + +- (void)testReload { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostApi reloadWebViewWithIdentifier:@0 error:&error]; + OCMVerify([mockWebView reload]); + XCTAssertNil(error); +} + +- (void)testTitle { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + OCMStub([mockWebView title]).andReturn(@"myTitle"); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + XCTAssertEqualObjects([hostApi titleForWebViewWithIdentifier:@0 error:&error], @"myTitle"); + XCTAssertNil(error); +} + +- (void)testSetAllowsBackForwardNavigationGestures { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostApi setAllowsBackForwardForWebViewWithIdentifier:@0 isAllowed:@YES error:&error]; + OCMVerify([mockWebView setAllowsBackForwardNavigationGestures:YES]); + XCTAssertNil(error); +} + +- (void)testEvaluateJavaScript { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + OCMStub([mockWebView + evaluateJavaScript:@"runJavaScript" + completionHandler:([OCMArg invokeBlockWithArgs:@"result", [NSNull null], nil])]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostApi = + [[FWFWebViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + NSString __block *returnValue; + FlutterError __block *returnError; + [hostApi evaluateJavaScriptForWebViewWithIdentifier:@0 + javaScriptString:@"runJavaScript" + completion:^(id result, FlutterError *error) { + returnValue = result; + returnError = error; + }]; + + XCTAssertEqualObjects(returnValue, @"result"); + XCTAssertNil(returnError); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart index d4e0ec4aba0c..e8b86d9c5773 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -9,7 +9,6 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; @@ -105,27 +104,23 @@ class _WebViewExampleState extends State<_WebViewExample> { _SampleMenu(_controller.future), ], ), - // We're using a Builder here so we have a context that is below the Scaffold - // to allow calling Scaffold.of(context) so we can show a snackbar. - body: Builder(builder: (BuildContext context) { - return WebView( - initialUrl: 'https://flutter.dev/', - onWebViewCreated: (WebViewController controller) { - _controller.complete(controller); - }, - javascriptChannels: _createJavascriptChannels(context), - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) { - if (request.url.startsWith('https://www.youtube.com/')) { - print('blocking navigation to $request}'); - return NavigationDecision.prevent; - } - print('allowing navigation to $request'); - return NavigationDecision.navigate; - }, - backgroundColor: const Color(0x80000000), - ); - }), + body: WebView( + initialUrl: 'https://flutter.dev/', + onWebViewCreated: (WebViewController controller) { + _controller.complete(controller); + }, + javascriptChannels: _createJavascriptChannels(context), + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $request}'); + return NavigationDecision.prevent; + } + print('allowing navigation to $request'); + return NavigationDecision.navigate; + }, + backgroundColor: const Color(0x80000000), + ), floatingActionButton: favoriteButton(), ); } @@ -139,8 +134,7 @@ class _WebViewExampleState extends State<_WebViewExample> { return FloatingActionButton( onPressed: () async { final String url = (await controller.data!.currentUrl())!; - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Favorited $url')), ); }, @@ -456,8 +450,7 @@ class _NavigationControls extends StatelessWidget { if (await controller!.canGoBack()) { await controller.goBack(); } else { - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('No back history item')), ); return; @@ -472,8 +465,7 @@ class _NavigationControls extends StatelessWidget { if (await controller!.canGoForward()) { await controller.goForward(); } else { - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('No forward history item')), ); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m index a4e87ba5177a..a63d6a60b114 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m @@ -4,6 +4,7 @@ #import "FLTWebViewFlutterPlugin.h" #import "FLTCookieManager.h" +#import "FWFWebViewHostApi.h" #import "FlutterWebView.h" @implementation FLTWebViewFlutterPlugin diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h new file mode 100644 index 000000000000..e9dbf2d9efa7 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FWFGeneratedWebKitApis.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Converts an FWFNSUrlRequestData to an NSURLRequest. + * + * @param data The data object containing information to create an NSURLRequest. + * + * @return An NSURLRequest or nil if data could not be converted. + */ +extern NSURLRequest* _Nullable FWFNSURLRequestFromRequestData( + FWFNSUrlRequestData* data); + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m new file mode 100644 index 000000000000..1945bc3354f3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FWFDataConverters.h" + +#import + +NSURLRequest *_Nullable FWFNSURLRequestFromRequestData(FWFNSUrlRequestData *data) { + NSURL *url = [NSURL URLWithString:data.url]; + if (!url) { + return nil; + } + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + if (!request) { + return nil; + } + + [request setHTTPMethod:data.httpMethod]; + [request setHTTPBody:data.httpBody.data]; + [request setAllHTTPHeaderFields:data.allHttpHeaderFields]; + + return request; +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h new file mode 100644 index 000000000000..5306b300fc03 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h @@ -0,0 +1,437 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.0.3), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import +@protocol FlutterBinaryMessenger; +@protocol FlutterMessageCodec; +@class FlutterError; +@class FlutterStandardTypedData; + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, FWFNSKeyValueObservingOptionsEnum) { + FWFNSKeyValueObservingOptionsEnumNewValue = 0, + FWFNSKeyValueObservingOptionsEnumOldValue = 1, + FWFNSKeyValueObservingOptionsEnumInitialValue = 2, + FWFNSKeyValueObservingOptionsEnumPriorNotification = 3, +}; + +typedef NS_ENUM(NSUInteger, FWFNSKeyValueChangeEnum) { + FWFNSKeyValueChangeEnumSetting = 0, + FWFNSKeyValueChangeEnumInsertion = 1, + FWFNSKeyValueChangeEnumRemoval = 2, + FWFNSKeyValueChangeEnumReplacement = 3, +}; + +typedef NS_ENUM(NSUInteger, FWFNSKeyValueChangeKeyEnum) { + FWFNSKeyValueChangeKeyEnumIndexes = 0, + FWFNSKeyValueChangeKeyEnumKind = 1, + FWFNSKeyValueChangeKeyEnumNewValue = 2, + FWFNSKeyValueChangeKeyEnumNotificationIsPrior = 3, + FWFNSKeyValueChangeKeyEnumOldValue = 4, +}; + +typedef NS_ENUM(NSUInteger, FWFWKUserScriptInjectionTimeEnum) { + FWFWKUserScriptInjectionTimeEnumAtDocumentStart = 0, + FWFWKUserScriptInjectionTimeEnumAtDocumentEnd = 1, +}; + +typedef NS_ENUM(NSUInteger, FWFWKAudiovisualMediaTypeEnum) { + FWFWKAudiovisualMediaTypeEnumNone = 0, + FWFWKAudiovisualMediaTypeEnumAudio = 1, + FWFWKAudiovisualMediaTypeEnumVideo = 2, + FWFWKAudiovisualMediaTypeEnumAll = 3, +}; + +typedef NS_ENUM(NSUInteger, FWFWKWebsiteDataTypesEnum) { + FWFWKWebsiteDataTypesEnumCookies = 0, + FWFWKWebsiteDataTypesEnumMemoryCache = 1, + FWFWKWebsiteDataTypesEnumDiskCache = 2, + FWFWKWebsiteDataTypesEnumOfflineWebApplicationCache = 3, + FWFWKWebsiteDataTypesEnumLocalStroage = 4, + FWFWKWebsiteDataTypesEnumSessionStorage = 5, + FWFWKWebsiteDataTypesEnumSqlDatabases = 6, + FWFWKWebsiteDataTypesEnumIndexedDBDatabases = 7, +}; + +typedef NS_ENUM(NSUInteger, FWFWKNavigationActionPolicyEnum) { + FWFWKNavigationActionPolicyEnumAllow = 0, + FWFWKNavigationActionPolicyEnumCancel = 1, +}; + +typedef NS_ENUM(NSUInteger, FWFNSHttpCookiePropertyKeyEnum) { + FWFNSHttpCookiePropertyKeyEnumComment = 0, + FWFNSHttpCookiePropertyKeyEnumCommentUrl = 1, + FWFNSHttpCookiePropertyKeyEnumDiscard = 2, + FWFNSHttpCookiePropertyKeyEnumDomain = 3, + FWFNSHttpCookiePropertyKeyEnumExpires = 4, + FWFNSHttpCookiePropertyKeyEnumMaximumAge = 5, + FWFNSHttpCookiePropertyKeyEnumName = 6, + FWFNSHttpCookiePropertyKeyEnumOriginUrl = 7, + FWFNSHttpCookiePropertyKeyEnumPath = 8, + FWFNSHttpCookiePropertyKeyEnumPort = 9, + FWFNSHttpCookiePropertyKeyEnumSameSitePolicy = 10, + FWFNSHttpCookiePropertyKeyEnumSecure = 11, + FWFNSHttpCookiePropertyKeyEnumValue = 12, + FWFNSHttpCookiePropertyKeyEnumVersion = 13, +}; + +@class FWFNSKeyValueObservingOptionsEnumData; +@class FWFWKUserScriptInjectionTimeEnumData; +@class FWFWKAudiovisualMediaTypeEnumData; +@class FWFWKWebsiteDataTypesEnumData; +@class FWFNSHttpCookiePropertyKeyEnumData; +@class FWFNSUrlRequestData; +@class FWFWKUserScriptData; +@class FWFNSHttpCookieData; + +@interface FWFNSKeyValueObservingOptionsEnumData : NSObject ++ (instancetype)makeWithValue:(FWFNSKeyValueObservingOptionsEnum)value; +@property(nonatomic, assign) FWFNSKeyValueObservingOptionsEnum value; +@end + +@interface FWFWKUserScriptInjectionTimeEnumData : NSObject ++ (instancetype)makeWithValue:(FWFWKUserScriptInjectionTimeEnum)value; +@property(nonatomic, assign) FWFWKUserScriptInjectionTimeEnum value; +@end + +@interface FWFWKAudiovisualMediaTypeEnumData : NSObject ++ (instancetype)makeWithValue:(FWFWKAudiovisualMediaTypeEnum)value; +@property(nonatomic, assign) FWFWKAudiovisualMediaTypeEnum value; +@end + +@interface FWFWKWebsiteDataTypesEnumData : NSObject ++ (instancetype)makeWithValue:(FWFWKWebsiteDataTypesEnum)value; +@property(nonatomic, assign) FWFWKWebsiteDataTypesEnum value; +@end + +@interface FWFNSHttpCookiePropertyKeyEnumData : NSObject ++ (instancetype)makeWithValue:(FWFNSHttpCookiePropertyKeyEnum)value; +@property(nonatomic, assign) FWFNSHttpCookiePropertyKeyEnum value; +@end + +@interface FWFNSUrlRequestData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithUrl:(NSString *)url + httpMethod:(nullable NSString *)httpMethod + httpBody:(nullable FlutterStandardTypedData *)httpBody + allHttpHeaderFields:(NSDictionary *)allHttpHeaderFields; +@property(nonatomic, copy) NSString *url; +@property(nonatomic, copy, nullable) NSString *httpMethod; +@property(nonatomic, strong, nullable) FlutterStandardTypedData *httpBody; +@property(nonatomic, strong) NSDictionary *allHttpHeaderFields; +@end + +@interface FWFWKUserScriptData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithSource:(NSString *)source + injectionTime:(nullable FWFWKUserScriptInjectionTimeEnumData *)injectionTime + isMainFrameOnly:(NSNumber *)isMainFrameOnly; +@property(nonatomic, copy) NSString *source; +@property(nonatomic, strong, nullable) FWFWKUserScriptInjectionTimeEnumData *injectionTime; +@property(nonatomic, strong) NSNumber *isMainFrameOnly; +@end + +@interface FWFNSHttpCookieData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithProperties: + (NSDictionary *)properties; +@property(nonatomic, strong) + NSDictionary *properties; +@end + +/// The codec used by FWFWKWebsiteDataStoreHostApi. +NSObject *FWFWKWebsiteDataStoreHostApiGetCodec(void); + +@protocol FWFWKWebsiteDataStoreHostApi +- (void)createDataStoreFromConfigurationWithIdentifier:(NSNumber *)instanceId + configurationIdentifier:(NSNumber *)configurationInstanceId + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)createDefaultDataStoreWithIdentifier:(NSNumber *)instanceId + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)removeDataFromDataStoreWithIdentifier:(NSNumber *)instanceId + ofTypes:(NSArray *)dataTypes + secondsModifiedSinceEpoch:(NSNumber *)secondsModifiedSinceEpoch + completion:(void (^)(NSNumber *_Nullable, + FlutterError *_Nullable))completion; +@end + +extern void FWFWKWebsiteDataStoreHostApiSetup( + id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFUIViewHostApi. +NSObject *FWFUIViewHostApiGetCodec(void); + +@protocol FWFUIViewHostApi +/// @return `nil` only when `error != nil`. +- (nullable NSArray *) + contentOffsetForViewWithIdentifier:(NSNumber *)instanceId + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setBackgroundColorForViewWithIdentifier:(NSNumber *)instanceId + toValue:(nullable NSNumber *)value + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setOpaqueForViewWithIdentifier:(NSNumber *)instanceId + isOpaque:(NSNumber *)opaque + error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFUIViewHostApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFUIScrollViewHostApi. +NSObject *FWFUIScrollViewHostApiGetCodec(void); + +@protocol FWFUIScrollViewHostApi +- (void)createFromWebViewWithIdentifier:(NSNumber *)instanceId + webViewIdentifier:(NSNumber *)webViewInstanceId + error:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSArray *) + contentOffsetForScrollViewWithIdentifier:(NSNumber *)instanceId + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)scrollByForScrollViewWithIdentifier:(NSNumber *)instanceId + toX:(NSNumber *)x + y:(NSNumber *)y + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setContentOffsetForScrollViewWithIdentifier:(NSNumber *)instanceId + toX:(NSNumber *)x + y:(NSNumber *)y + error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFUIScrollViewHostApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFWKWebViewConfigurationHostApi. +NSObject *FWFWKWebViewConfigurationHostApiGetCodec(void); + +@protocol FWFWKWebViewConfigurationHostApi +- (void)createWithIdentifier:(NSNumber *)instanceId error:(FlutterError *_Nullable *_Nonnull)error; +- (void)createFromWebViewWithIdentifier:(NSNumber *)instanceId + webViewIdentifier:(NSNumber *)webViewInstanceId + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setAllowsInlineMediaPlaybackForConfigurationWithIdentifier:(NSNumber *)instanceId + isAlowed:(NSNumber *)allow + error: + (FlutterError *_Nullable *_Nonnull) + error; +- (void) + setMediaTypesRequiresUserActionForConfigurationWithIdentifier:(NSNumber *)instanceId + forTypes: + (NSArray< + FWFWKAudiovisualMediaTypeEnumData + *> *)types + error: + (FlutterError *_Nullable *_Nonnull) + error; +@end + +extern void FWFWKWebViewConfigurationHostApiSetup( + id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFWKUserContentControllerHostApi. +NSObject *FWFWKUserContentControllerHostApiGetCodec(void); + +@protocol FWFWKUserContentControllerHostApi +- (void)createFromWebViewConfigurationWithIdentifier:(NSNumber *)instanceId + configurationIdentifier:(NSNumber *)configurationInstanceId + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)addScriptMessageHandlerForControllerWithIdentifier:(NSNumber *)instanceId + handlerIdentifier:(NSNumber *)handlerInstanceid + ofName:(NSString *)name + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)removeScriptMessageHandlerForControllerWithIdentifier:(NSNumber *)instanceId + name:(NSString *)name + error:(FlutterError *_Nullable *_Nonnull) + error; +- (void)removeAllScriptMessageHandlersForControllerWithIdentifier:(NSNumber *)instanceId + error: + (FlutterError *_Nullable *_Nonnull) + error; +- (void)addUserScriptForControllerWithIdentifier:(NSNumber *)instanceId + userScript:(FWFWKUserScriptData *)userScript + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)removeAllUserScriptsForControllerWithIdentifier:(NSNumber *)instanceId + error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFWKUserContentControllerHostApiSetup( + id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFWKPreferencesHostApi. +NSObject *FWFWKPreferencesHostApiGetCodec(void); + +@protocol FWFWKPreferencesHostApi +- (void)createFromWebViewConfiguration:(NSNumber *)instanceId + configurationIdentifier:(NSNumber *)configurationInstanceId + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setJavaScriptEnabledForPreferencesWithIdentifier:(NSNumber *)instanceId + isEnabled:(NSNumber *)enabled + error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFWKPreferencesHostApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFWKScriptMessageHandlerHostApi. +NSObject *FWFWKScriptMessageHandlerHostApiGetCodec(void); + +@protocol FWFWKScriptMessageHandlerHostApi +- (void)createWithIdentifier:(NSNumber *)instanceId error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFWKScriptMessageHandlerHostApiSetup( + id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFWKNavigationDelegateHostApi. +NSObject *FWFWKNavigationDelegateHostApiGetCodec(void); + +@protocol FWFWKNavigationDelegateHostApi +- (void)createWithIdentifier:(NSNumber *)instanceId error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setDidFinishNavigationForDelegateWithIdentifier:(NSNumber *)instanceId + functionIdentifier:(nullable NSNumber *)functionInstanceId + error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFWKNavigationDelegateHostApiSetup( + id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFWKNavigationDelegateFlutterApi. +NSObject *FWFWKNavigationDelegateFlutterApiGetCodec(void); + +@interface FWFWKNavigationDelegateFlutterApi : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger; +- (void)didFinishNavigationForDelegateWithIdentifier:(NSNumber *)functionInstanceId + webViewIdentifier:(NSNumber *)webViewInstanceId + URL:(nullable NSString *)url + completion:(void (^)(NSError *_Nullable))completion; +@end +/// The codec used by FWFNSObjectHostApi. +NSObject *FWFNSObjectHostApiGetCodec(void); + +@protocol FWFNSObjectHostApi +- (void)disposeObjectWithIdentifier:(NSNumber *)instanceId + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)addObserverForObjectWithIdentifier:(NSNumber *)instanceId + observerIdentifier:(NSNumber *)observerInstanceId + keyPath:(NSString *)keyPath + options: + (NSArray *)options + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)removeObserverForObjectWithIdentifier:(NSNumber *)instanceId + observerIdentifier:(NSNumber *)observerInstanceId + keyPath:(NSString *)keyPath + error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFNSObjectHostApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFFunctionFlutterApi. +NSObject *FWFFunctionFlutterApiGetCodec(void); + +@interface FWFFunctionFlutterApi : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger; +- (void)disposeFunctionWithIdentifier:(NSNumber *)instanceId + completion:(void (^)(NSError *_Nullable))completion; +@end +/// The codec used by FWFWKWebViewHostApi. +NSObject *FWFWKWebViewHostApiGetCodec(void); + +@protocol FWFWKWebViewHostApi +- (void)createWithIdentifier:(NSNumber *)instanceId + configurationIdentifier:(NSNumber *)configurationInstanceId + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setUIDelegateForWebViewWithIdentifier:(NSNumber *)instanceId + delegateIdentifier:(nullable NSNumber *)uiDelegateInstanceId + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setNavigationDelegateForWebViewWithIdentifier:(NSNumber *)instanceId + delegateIdentifier: + (nullable NSNumber *)navigationDelegateInstanceId + error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSString *)URLForWebViewWithIdentifier:(NSNumber *)instanceId + error:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)estimatedProgressForWebViewWithIdentifier:(NSNumber *)instanceId + error:(FlutterError *_Nullable *_Nonnull) + error; +- (void)loadRequestForWebViewWithIdentifier:(NSNumber *)instanceId + request:(FWFNSUrlRequestData *)request + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)loadHTMLForWebViewWithIdentifier:(NSNumber *)instanceId + HTMLString:(NSString *)string + baseURL:(nullable NSString *)baseUrl + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)loadFileForWebViewWithIdentifier:(NSNumber *)instanceId + fileURL:(NSString *)url + readAccessURL:(NSString *)readAccessUrl + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)loadAssetForWebViewWithIdentifier:(NSNumber *)instanceId + assetKey:(NSString *)key + error:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)canGoBackForWebViewWithIdentifier:(NSNumber *)instanceId + error:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)canGoForwardForWebViewWithIdentifier:(NSNumber *)instanceId + error: + (FlutterError *_Nullable *_Nonnull)error; +- (void)goBackForWebViewWithIdentifier:(NSNumber *)instanceId + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)goForwardForWebViewWithIdentifier:(NSNumber *)instanceId + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)reloadWebViewWithIdentifier:(NSNumber *)instanceId + error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSString *)titleForWebViewWithIdentifier:(NSNumber *)instanceId + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setAllowsBackForwardForWebViewWithIdentifier:(NSNumber *)instanceId + isAllowed:(NSNumber *)allow + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setUserAgentForWebViewWithIdentifier:(NSNumber *)instanceId + userAgent:(nullable NSString *)userAgent + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)evaluateJavaScriptForWebViewWithIdentifier:(NSNumber *)instanceId + javaScriptString:(NSString *)javaScriptString + completion:(void (^)(id _Nullable, + FlutterError *_Nullable))completion; +@end + +extern void FWFWKWebViewHostApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFWKUIDelegateHostApi. +NSObject *FWFWKUIDelegateHostApiGetCodec(void); + +@protocol FWFWKUIDelegateHostApi +- (void)createWithIdentifier:(NSNumber *)instanceId error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFWKUIDelegateHostApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFWKHttpCookieStoreHostApi. +NSObject *FWFWKHttpCookieStoreHostApiGetCodec(void); + +@protocol FWFWKHttpCookieStoreHostApi +- (void)createFromWebsiteDataStoreWithIdentifier:(NSNumber *)instanceId + dataStoreIdentifier:(NSNumber *)websiteDataStoreInstanceId + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setCookieForStoreWithIdentifier:(NSNumber *)instanceId + cookie:(FWFNSHttpCookieData *)cookie + error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFWKHttpCookieStoreHostApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m new file mode 100644 index 000000000000..070b3c5cc033 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m @@ -0,0 +1,2126 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.0.3), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import "FWFGeneratedWebKitApis.h" +#import + +#if !__has_feature(objc_arc) +#error File requires ARC to be enabled. +#endif + +static NSDictionary *wrapResult(id result, FlutterError *error) { + NSDictionary *errorDict = (NSDictionary *)[NSNull null]; + if (error) { + errorDict = @{ + @"code" : (error.code ? error.code : [NSNull null]), + @"message" : (error.message ? error.message : [NSNull null]), + @"details" : (error.details ? error.details : [NSNull null]), + }; + } + return @{ + @"result" : (result ? result : [NSNull null]), + @"error" : errorDict, + }; +} +static id GetNullableObject(NSDictionary *dict, id key) { + id result = dict[key]; + return (result == [NSNull null]) ? nil : result; +} +static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { + id result = array[key]; + return (result == [NSNull null]) ? nil : result; +} + +@interface FWFNSKeyValueObservingOptionsEnumData () ++ (FWFNSKeyValueObservingOptionsEnumData *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFWKUserScriptInjectionTimeEnumData () ++ (FWFWKUserScriptInjectionTimeEnumData *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFWKAudiovisualMediaTypeEnumData () ++ (FWFWKAudiovisualMediaTypeEnumData *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFWKWebsiteDataTypesEnumData () ++ (FWFWKWebsiteDataTypesEnumData *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFNSHttpCookiePropertyKeyEnumData () ++ (FWFNSHttpCookiePropertyKeyEnumData *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFNSUrlRequestData () ++ (FWFNSUrlRequestData *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFWKUserScriptData () ++ (FWFWKUserScriptData *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFNSHttpCookieData () ++ (FWFNSHttpCookieData *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end + +@implementation FWFNSKeyValueObservingOptionsEnumData ++ (instancetype)makeWithValue:(FWFNSKeyValueObservingOptionsEnum)value { + FWFNSKeyValueObservingOptionsEnumData *pigeonResult = + [[FWFNSKeyValueObservingOptionsEnumData alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FWFNSKeyValueObservingOptionsEnumData *)fromMap:(NSDictionary *)dict { + FWFNSKeyValueObservingOptionsEnumData *pigeonResult = + [[FWFNSKeyValueObservingOptionsEnumData alloc] init]; + pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary dictionaryWithObjectsAndKeys:@(self.value), @"value", nil]; +} +@end + +@implementation FWFWKUserScriptInjectionTimeEnumData ++ (instancetype)makeWithValue:(FWFWKUserScriptInjectionTimeEnum)value { + FWFWKUserScriptInjectionTimeEnumData *pigeonResult = + [[FWFWKUserScriptInjectionTimeEnumData alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FWFWKUserScriptInjectionTimeEnumData *)fromMap:(NSDictionary *)dict { + FWFWKUserScriptInjectionTimeEnumData *pigeonResult = + [[FWFWKUserScriptInjectionTimeEnumData alloc] init]; + pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary dictionaryWithObjectsAndKeys:@(self.value), @"value", nil]; +} +@end + +@implementation FWFWKAudiovisualMediaTypeEnumData ++ (instancetype)makeWithValue:(FWFWKAudiovisualMediaTypeEnum)value { + FWFWKAudiovisualMediaTypeEnumData *pigeonResult = + [[FWFWKAudiovisualMediaTypeEnumData alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FWFWKAudiovisualMediaTypeEnumData *)fromMap:(NSDictionary *)dict { + FWFWKAudiovisualMediaTypeEnumData *pigeonResult = + [[FWFWKAudiovisualMediaTypeEnumData alloc] init]; + pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary dictionaryWithObjectsAndKeys:@(self.value), @"value", nil]; +} +@end + +@implementation FWFWKWebsiteDataTypesEnumData ++ (instancetype)makeWithValue:(FWFWKWebsiteDataTypesEnum)value { + FWFWKWebsiteDataTypesEnumData *pigeonResult = [[FWFWKWebsiteDataTypesEnumData alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FWFWKWebsiteDataTypesEnumData *)fromMap:(NSDictionary *)dict { + FWFWKWebsiteDataTypesEnumData *pigeonResult = [[FWFWKWebsiteDataTypesEnumData alloc] init]; + pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary dictionaryWithObjectsAndKeys:@(self.value), @"value", nil]; +} +@end + +@implementation FWFNSHttpCookiePropertyKeyEnumData ++ (instancetype)makeWithValue:(FWFNSHttpCookiePropertyKeyEnum)value { + FWFNSHttpCookiePropertyKeyEnumData *pigeonResult = + [[FWFNSHttpCookiePropertyKeyEnumData alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FWFNSHttpCookiePropertyKeyEnumData *)fromMap:(NSDictionary *)dict { + FWFNSHttpCookiePropertyKeyEnumData *pigeonResult = + [[FWFNSHttpCookiePropertyKeyEnumData alloc] init]; + pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary dictionaryWithObjectsAndKeys:@(self.value), @"value", nil]; +} +@end + +@implementation FWFNSUrlRequestData ++ (instancetype)makeWithUrl:(NSString *)url + httpMethod:(nullable NSString *)httpMethod + httpBody:(nullable FlutterStandardTypedData *)httpBody + allHttpHeaderFields:(NSDictionary *)allHttpHeaderFields { + FWFNSUrlRequestData *pigeonResult = [[FWFNSUrlRequestData alloc] init]; + pigeonResult.url = url; + pigeonResult.httpMethod = httpMethod; + pigeonResult.httpBody = httpBody; + pigeonResult.allHttpHeaderFields = allHttpHeaderFields; + return pigeonResult; +} ++ (FWFNSUrlRequestData *)fromMap:(NSDictionary *)dict { + FWFNSUrlRequestData *pigeonResult = [[FWFNSUrlRequestData alloc] init]; + pigeonResult.url = GetNullableObject(dict, @"url"); + NSAssert(pigeonResult.url != nil, @""); + pigeonResult.httpMethod = GetNullableObject(dict, @"httpMethod"); + pigeonResult.httpBody = GetNullableObject(dict, @"httpBody"); + pigeonResult.allHttpHeaderFields = GetNullableObject(dict, @"allHttpHeaderFields"); + NSAssert(pigeonResult.allHttpHeaderFields != nil, @""); + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.url ? self.url : [NSNull null]), @"url", + (self.httpMethod ? self.httpMethod : [NSNull null]), + @"httpMethod", (self.httpBody ? self.httpBody : [NSNull null]), + @"httpBody", + (self.allHttpHeaderFields ? self.allHttpHeaderFields + : [NSNull null]), + @"allHttpHeaderFields", nil]; +} +@end + +@implementation FWFWKUserScriptData ++ (instancetype)makeWithSource:(NSString *)source + injectionTime:(nullable FWFWKUserScriptInjectionTimeEnumData *)injectionTime + isMainFrameOnly:(NSNumber *)isMainFrameOnly { + FWFWKUserScriptData *pigeonResult = [[FWFWKUserScriptData alloc] init]; + pigeonResult.source = source; + pigeonResult.injectionTime = injectionTime; + pigeonResult.isMainFrameOnly = isMainFrameOnly; + return pigeonResult; +} ++ (FWFWKUserScriptData *)fromMap:(NSDictionary *)dict { + FWFWKUserScriptData *pigeonResult = [[FWFWKUserScriptData alloc] init]; + pigeonResult.source = GetNullableObject(dict, @"source"); + NSAssert(pigeonResult.source != nil, @""); + pigeonResult.injectionTime = + [FWFWKUserScriptInjectionTimeEnumData fromMap:GetNullableObject(dict, @"injectionTime")]; + pigeonResult.isMainFrameOnly = GetNullableObject(dict, @"isMainFrameOnly"); + NSAssert(pigeonResult.isMainFrameOnly != nil, @""); + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.source ? self.source : [NSNull null]), @"source", + (self.injectionTime ? [self.injectionTime toMap] + : [NSNull null]), + @"injectionTime", + (self.isMainFrameOnly ? self.isMainFrameOnly : [NSNull null]), + @"isMainFrameOnly", nil]; +} +@end + +@implementation FWFNSHttpCookieData ++ (instancetype)makeWithProperties: + (NSDictionary *)properties { + FWFNSHttpCookieData *pigeonResult = [[FWFNSHttpCookieData alloc] init]; + pigeonResult.properties = properties; + return pigeonResult; +} ++ (FWFNSHttpCookieData *)fromMap:(NSDictionary *)dict { + FWFNSHttpCookieData *pigeonResult = [[FWFNSHttpCookieData alloc] init]; + pigeonResult.properties = GetNullableObject(dict, @"properties"); + NSAssert(pigeonResult.properties != nil, @""); + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.properties ? self.properties : [NSNull null]), + @"properties", nil]; +} +@end + +@interface FWFWKWebsiteDataStoreHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKWebsiteDataStoreHostApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FWFWKWebsiteDataTypesEnumData fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FWFWKWebsiteDataStoreHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKWebsiteDataStoreHostApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FWFWKWebsiteDataTypesEnumData class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FWFWKWebsiteDataStoreHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKWebsiteDataStoreHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKWebsiteDataStoreHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKWebsiteDataStoreHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKWebsiteDataStoreHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKWebsiteDataStoreHostApiCodecReaderWriter *readerWriter = + [[FWFWKWebsiteDataStoreHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKWebsiteDataStoreHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + @"dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createFromWebViewConfiguration" + binaryMessenger:binaryMessenger + codec:FWFWKWebsiteDataStoreHostApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(createDataStoreFromConfigurationWithIdentifier: + configurationIdentifier:error:)], + @"FWFWKWebsiteDataStoreHostApi api (%@) doesn't respond to " + @"@selector(createDataStoreFromConfigurationWithIdentifier:configurationIdentifier:error:" + @")", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_configurationInstanceId = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api createDataStoreFromConfigurationWithIdentifier:arg_instanceId + configurationIdentifier:arg_configurationInstanceId + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createDefaultDataStore" + binaryMessenger:binaryMessenger + codec:FWFWKWebsiteDataStoreHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createDefaultDataStoreWithIdentifier:error:)], + @"FWFWKWebsiteDataStoreHostApi api (%@) doesn't respond to " + @"@selector(createDefaultDataStoreWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api createDefaultDataStoreWithIdentifier:arg_instanceId error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebsiteDataStoreHostApi.removeDataOfTypes" + binaryMessenger:binaryMessenger + codec:FWFWKWebsiteDataStoreHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (removeDataFromDataStoreWithIdentifier: + ofTypes:secondsModifiedSinceEpoch:completion:)], + @"FWFWKWebsiteDataStoreHostApi api (%@) doesn't respond to " + @"@selector(removeDataFromDataStoreWithIdentifier:ofTypes:" + @"secondsModifiedSinceEpoch:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSArray *arg_dataTypes = GetNullableObjectAtIndex(args, 1); + NSNumber *arg_secondsModifiedSinceEpoch = GetNullableObjectAtIndex(args, 2); + [api removeDataFromDataStoreWithIdentifier:arg_instanceId + ofTypes:arg_dataTypes + secondsModifiedSinceEpoch:arg_secondsModifiedSinceEpoch + completion:^(NSNumber *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFUIViewHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFUIViewHostApiCodecReader +@end + +@interface FWFUIViewHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFUIViewHostApiCodecWriter +@end + +@interface FWFUIViewHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFUIViewHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFUIViewHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFUIViewHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFUIViewHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFUIViewHostApiCodecReaderWriter *readerWriter = + [[FWFUIViewHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFUIViewHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.UIViewHostApi.getContentOffset" + binaryMessenger:binaryMessenger + codec:FWFUIViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(contentOffsetForViewWithIdentifier:error:)], + @"FWFUIViewHostApi api (%@) doesn't respond to " + @"@selector(contentOffsetForViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + NSArray *output = [api contentOffsetForViewWithIdentifier:arg_instanceId + error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.UIViewHostApi.setBackgroundColor" + binaryMessenger:binaryMessenger + codec:FWFUIViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setBackgroundColorForViewWithIdentifier: + toValue:error:)], + @"FWFUIViewHostApi api (%@) doesn't respond to " + @"@selector(setBackgroundColorForViewWithIdentifier:toValue:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_value = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setBackgroundColorForViewWithIdentifier:arg_instanceId toValue:arg_value error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.UIViewHostApi.setOpaque" + binaryMessenger:binaryMessenger + codec:FWFUIViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setOpaqueForViewWithIdentifier:isOpaque:error:)], + @"FWFUIViewHostApi api (%@) doesn't respond to " + @"@selector(setOpaqueForViewWithIdentifier:isOpaque:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_opaque = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setOpaqueForViewWithIdentifier:arg_instanceId isOpaque:arg_opaque error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFUIScrollViewHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFUIScrollViewHostApiCodecReader +@end + +@interface FWFUIScrollViewHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFUIScrollViewHostApiCodecWriter +@end + +@interface FWFUIScrollViewHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFUIScrollViewHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFUIScrollViewHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFUIScrollViewHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFUIScrollViewHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFUIScrollViewHostApiCodecReaderWriter *readerWriter = + [[FWFUIScrollViewHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFUIScrollViewHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.UIScrollViewHostApi.createFromWebView" + binaryMessenger:binaryMessenger + codec:FWFUIScrollViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createFromWebViewWithIdentifier: + webViewIdentifier:error:)], + @"FWFUIScrollViewHostApi api (%@) doesn't respond to " + @"@selector(createFromWebViewWithIdentifier:webViewIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_webViewInstanceId = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api createFromWebViewWithIdentifier:arg_instanceId + webViewIdentifier:arg_webViewInstanceId + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.UIScrollViewHostApi.getContentOffset" + binaryMessenger:binaryMessenger + codec:FWFUIScrollViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(contentOffsetForScrollViewWithIdentifier:error:)], + @"FWFUIScrollViewHostApi api (%@) doesn't respond to " + @"@selector(contentOffsetForScrollViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + NSArray *output = [api contentOffsetForScrollViewWithIdentifier:arg_instanceId + error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.UIScrollViewHostApi.scrollBy" + binaryMessenger:binaryMessenger + codec:FWFUIScrollViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(scrollByForScrollViewWithIdentifier: + toX:y:error:)], + @"FWFUIScrollViewHostApi api (%@) doesn't respond to " + @"@selector(scrollByForScrollViewWithIdentifier:toX:y:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_x = GetNullableObjectAtIndex(args, 1); + NSNumber *arg_y = GetNullableObjectAtIndex(args, 2); + FlutterError *error; + [api scrollByForScrollViewWithIdentifier:arg_instanceId toX:arg_x y:arg_y error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.UIScrollViewHostApi.setContentOffset" + binaryMessenger:binaryMessenger + codec:FWFUIScrollViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (setContentOffsetForScrollViewWithIdentifier:toX:y:error:)], + @"FWFUIScrollViewHostApi api (%@) doesn't respond to " + @"@selector(setContentOffsetForScrollViewWithIdentifier:toX:y:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_x = GetNullableObjectAtIndex(args, 1); + NSNumber *arg_y = GetNullableObjectAtIndex(args, 2); + FlutterError *error; + [api setContentOffsetForScrollViewWithIdentifier:arg_instanceId + toX:arg_x + y:arg_y + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFWKWebViewConfigurationHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKWebViewConfigurationHostApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FWFWKAudiovisualMediaTypeEnumData fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FWFWKWebViewConfigurationHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKWebViewConfigurationHostApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FWFWKAudiovisualMediaTypeEnumData class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FWFWKWebViewConfigurationHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKWebViewConfigurationHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKWebViewConfigurationHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKWebViewConfigurationHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKWebViewConfigurationHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKWebViewConfigurationHostApiCodecReaderWriter *readerWriter = + [[FWFWKWebViewConfigurationHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKWebViewConfigurationHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewConfigurationHostApi.create" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewConfigurationHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createWithIdentifier:error:)], + @"FWFWKWebViewConfigurationHostApi api (%@) doesn't respond to " + @"@selector(createWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api createWithIdentifier:arg_instanceId error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewConfigurationHostApi.createFromWebView" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewConfigurationHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createFromWebViewWithIdentifier: + webViewIdentifier:error:)], + @"FWFWKWebViewConfigurationHostApi api (%@) doesn't respond to " + @"@selector(createFromWebViewWithIdentifier:webViewIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_webViewInstanceId = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api createFromWebViewWithIdentifier:arg_instanceId + webViewIdentifier:arg_webViewInstanceId + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + @"dev.flutter.pigeon.WKWebViewConfigurationHostApi.setAllowsInlineMediaPlayback" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewConfigurationHostApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector + (setAllowsInlineMediaPlaybackForConfigurationWithIdentifier:isAlowed:error:)], + @"FWFWKWebViewConfigurationHostApi api (%@) doesn't respond to " + @"@selector(setAllowsInlineMediaPlaybackForConfigurationWithIdentifier:isAlowed:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_allow = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setAllowsInlineMediaPlaybackForConfigurationWithIdentifier:arg_instanceId + isAlowed:arg_allow + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewConfigurationHostApi." + @"setMediaTypesRequiringUserActionForPlayback" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewConfigurationHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (setMediaTypesRequiresUserActionForConfigurationWithIdentifier: + forTypes:error:)], + @"FWFWKWebViewConfigurationHostApi api (%@) doesn't respond to " + @"@selector(setMediaTypesRequiresUserActionForConfigurationWithIdentifier:forTypes:" + @"error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSArray *arg_types = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setMediaTypesRequiresUserActionForConfigurationWithIdentifier:arg_instanceId + forTypes:arg_types + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFWKUserContentControllerHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKUserContentControllerHostApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FWFWKUserScriptData fromMap:[self readValue]]; + + case 129: + return [FWFWKUserScriptInjectionTimeEnumData fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FWFWKUserContentControllerHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKUserContentControllerHostApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FWFWKUserScriptData class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKUserScriptInjectionTimeEnumData class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FWFWKUserContentControllerHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKUserContentControllerHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKUserContentControllerHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKUserContentControllerHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKUserContentControllerHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKUserContentControllerHostApiCodecReaderWriter *readerWriter = + [[FWFWKUserContentControllerHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKUserContentControllerHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + @"dev.flutter.pigeon.WKUserContentControllerHostApi.createFromWebViewConfiguration" + binaryMessenger:binaryMessenger + codec:FWFWKUserContentControllerHostApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(createFromWebViewConfigurationWithIdentifier: + configurationIdentifier:error:)], + @"FWFWKUserContentControllerHostApi api (%@) doesn't respond to " + @"@selector(createFromWebViewConfigurationWithIdentifier:configurationIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_configurationInstanceId = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api createFromWebViewConfigurationWithIdentifier:arg_instanceId + configurationIdentifier:arg_configurationInstanceId + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKUserContentControllerHostApi.addScriptMessageHandler" + binaryMessenger:binaryMessenger + codec:FWFWKUserContentControllerHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (addScriptMessageHandlerForControllerWithIdentifier: + handlerIdentifier:ofName:error:)], + @"FWFWKUserContentControllerHostApi api (%@) doesn't respond to " + @"@selector(addScriptMessageHandlerForControllerWithIdentifier:handlerIdentifier:" + @"ofName:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_handlerInstanceid = GetNullableObjectAtIndex(args, 1); + NSString *arg_name = GetNullableObjectAtIndex(args, 2); + FlutterError *error; + [api addScriptMessageHandlerForControllerWithIdentifier:arg_instanceId + handlerIdentifier:arg_handlerInstanceid + ofName:arg_name + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + @"dev.flutter.pigeon.WKUserContentControllerHostApi.removeScriptMessageHandler" + binaryMessenger:binaryMessenger + codec:FWFWKUserContentControllerHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (removeScriptMessageHandlerForControllerWithIdentifier:name:error:)], + @"FWFWKUserContentControllerHostApi api (%@) doesn't respond to " + @"@selector(removeScriptMessageHandlerForControllerWithIdentifier:name:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSString *arg_name = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api removeScriptMessageHandlerForControllerWithIdentifier:arg_instanceId + name:arg_name + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + @"dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllScriptMessageHandlers" + binaryMessenger:binaryMessenger + codec:FWFWKUserContentControllerHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (removeAllScriptMessageHandlersForControllerWithIdentifier:error:)], + @"FWFWKUserContentControllerHostApi api (%@) doesn't respond to " + @"@selector(removeAllScriptMessageHandlersForControllerWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api removeAllScriptMessageHandlersForControllerWithIdentifier:arg_instanceId error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKUserContentControllerHostApi.addUserScript" + binaryMessenger:binaryMessenger + codec:FWFWKUserContentControllerHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(addUserScriptForControllerWithIdentifier: + userScript:error:)], + @"FWFWKUserContentControllerHostApi api (%@) doesn't respond to " + @"@selector(addUserScriptForControllerWithIdentifier:userScript:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FWFWKUserScriptData *arg_userScript = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api addUserScriptForControllerWithIdentifier:arg_instanceId + userScript:arg_userScript + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllUserScripts" + binaryMessenger:binaryMessenger + codec:FWFWKUserContentControllerHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (removeAllUserScriptsForControllerWithIdentifier:error:)], + @"FWFWKUserContentControllerHostApi api (%@) doesn't respond to " + @"@selector(removeAllUserScriptsForControllerWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api removeAllUserScriptsForControllerWithIdentifier:arg_instanceId error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFWKPreferencesHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKPreferencesHostApiCodecReader +@end + +@interface FWFWKPreferencesHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKPreferencesHostApiCodecWriter +@end + +@interface FWFWKPreferencesHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKPreferencesHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKPreferencesHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKPreferencesHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKPreferencesHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKPreferencesHostApiCodecReaderWriter *readerWriter = + [[FWFWKPreferencesHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKPreferencesHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKPreferencesHostApi.createFromWebViewConfiguration" + binaryMessenger:binaryMessenger + codec:FWFWKPreferencesHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createFromWebViewConfiguration: + configurationIdentifier:error:)], + @"FWFWKPreferencesHostApi api (%@) doesn't respond to " + @"@selector(createFromWebViewConfiguration:configurationIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_configurationInstanceId = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api createFromWebViewConfiguration:arg_instanceId + configurationIdentifier:arg_configurationInstanceId + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKPreferencesHostApi.setJavaScriptEnabled" + binaryMessenger:binaryMessenger + codec:FWFWKPreferencesHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (setJavaScriptEnabledForPreferencesWithIdentifier:isEnabled:error:)], + @"FWFWKPreferencesHostApi api (%@) doesn't respond to " + @"@selector(setJavaScriptEnabledForPreferencesWithIdentifier:isEnabled:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_enabled = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setJavaScriptEnabledForPreferencesWithIdentifier:arg_instanceId + isEnabled:arg_enabled + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFWKScriptMessageHandlerHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKScriptMessageHandlerHostApiCodecReader +@end + +@interface FWFWKScriptMessageHandlerHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKScriptMessageHandlerHostApiCodecWriter +@end + +@interface FWFWKScriptMessageHandlerHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKScriptMessageHandlerHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKScriptMessageHandlerHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKScriptMessageHandlerHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKScriptMessageHandlerHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKScriptMessageHandlerHostApiCodecReaderWriter *readerWriter = + [[FWFWKScriptMessageHandlerHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKScriptMessageHandlerHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKScriptMessageHandlerHostApi.create" + binaryMessenger:binaryMessenger + codec:FWFWKScriptMessageHandlerHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createWithIdentifier:error:)], + @"FWFWKScriptMessageHandlerHostApi api (%@) doesn't respond to " + @"@selector(createWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api createWithIdentifier:arg_instanceId error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFWKNavigationDelegateHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKNavigationDelegateHostApiCodecReader +@end + +@interface FWFWKNavigationDelegateHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKNavigationDelegateHostApiCodecWriter +@end + +@interface FWFWKNavigationDelegateHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKNavigationDelegateHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKNavigationDelegateHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKNavigationDelegateHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKNavigationDelegateHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKNavigationDelegateHostApiCodecReaderWriter *readerWriter = + [[FWFWKNavigationDelegateHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKNavigationDelegateHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKNavigationDelegateHostApi.create" + binaryMessenger:binaryMessenger + codec:FWFWKNavigationDelegateHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createWithIdentifier:error:)], + @"FWFWKNavigationDelegateHostApi api (%@) doesn't respond to " + @"@selector(createWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api createWithIdentifier:arg_instanceId error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKNavigationDelegateHostApi.setDidFinishNavigation" + binaryMessenger:binaryMessenger + codec:FWFWKNavigationDelegateHostApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector + (setDidFinishNavigationForDelegateWithIdentifier:functionIdentifier:error:)], + @"FWFWKNavigationDelegateHostApi api (%@) doesn't respond to " + @"@selector(setDidFinishNavigationForDelegateWithIdentifier:functionIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_functionInstanceId = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setDidFinishNavigationForDelegateWithIdentifier:arg_instanceId + functionIdentifier:arg_functionInstanceId + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFWKNavigationDelegateFlutterApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKNavigationDelegateFlutterApiCodecReader +@end + +@interface FWFWKNavigationDelegateFlutterApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKNavigationDelegateFlutterApiCodecWriter +@end + +@interface FWFWKNavigationDelegateFlutterApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKNavigationDelegateFlutterApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKNavigationDelegateFlutterApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKNavigationDelegateFlutterApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKNavigationDelegateFlutterApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKNavigationDelegateFlutterApiCodecReaderWriter *readerWriter = + [[FWFWKNavigationDelegateFlutterApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +@interface FWFWKNavigationDelegateFlutterApi () +@property(nonatomic, strong) NSObject *binaryMessenger; +@end + +@implementation FWFWKNavigationDelegateFlutterApi + +- (instancetype)initWithBinaryMessenger:(NSObject *)binaryMessenger { + self = [super init]; + if (self) { + _binaryMessenger = binaryMessenger; + } + return self; +} +- (void)didFinishNavigationForDelegateWithIdentifier:(NSNumber *)arg_functionInstanceId + webViewIdentifier:(NSNumber *)arg_webViewInstanceId + URL:(nullable NSString *)arg_url + completion:(void (^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName: + @"dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFinishNavigation" + binaryMessenger:self.binaryMessenger + codec:FWFWKNavigationDelegateFlutterApiGetCodec()]; + [channel sendMessage:@[ + (arg_functionInstanceId == nil) ? [NSNull null] : arg_functionInstanceId, + (arg_webViewInstanceId == nil) ? [NSNull null] : arg_webViewInstanceId, + (arg_url == nil) ? [NSNull null] : arg_url + ] + reply:^(id reply) { + completion(nil); + }]; +} +@end +@interface FWFNSObjectHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFNSObjectHostApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FWFNSKeyValueObservingOptionsEnumData fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FWFNSObjectHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFNSObjectHostApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FWFNSKeyValueObservingOptionsEnumData class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FWFNSObjectHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFNSObjectHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFNSObjectHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFNSObjectHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFNSObjectHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFNSObjectHostApiCodecReaderWriter *readerWriter = + [[FWFNSObjectHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFNSObjectHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NSObjectHostApi.dispose" + binaryMessenger:binaryMessenger + codec:FWFNSObjectHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(disposeObjectWithIdentifier:error:)], + @"FWFNSObjectHostApi api (%@) doesn't respond to " + @"@selector(disposeObjectWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api disposeObjectWithIdentifier:arg_instanceId error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NSObjectHostApi.addObserver" + binaryMessenger:binaryMessenger + codec:FWFNSObjectHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (addObserverForObjectWithIdentifier: + observerIdentifier:keyPath:options:error:)], + @"FWFNSObjectHostApi api (%@) doesn't respond to " + @"@selector(addObserverForObjectWithIdentifier:observerIdentifier:keyPath:options:" + @"error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_observerInstanceId = GetNullableObjectAtIndex(args, 1); + NSString *arg_keyPath = GetNullableObjectAtIndex(args, 2); + NSArray *arg_options = + GetNullableObjectAtIndex(args, 3); + FlutterError *error; + [api addObserverForObjectWithIdentifier:arg_instanceId + observerIdentifier:arg_observerInstanceId + keyPath:arg_keyPath + options:arg_options + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NSObjectHostApi.removeObserver" + binaryMessenger:binaryMessenger + codec:FWFNSObjectHostApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(removeObserverForObjectWithIdentifier: + observerIdentifier:keyPath:error:)], + @"FWFNSObjectHostApi api (%@) doesn't respond to " + @"@selector(removeObserverForObjectWithIdentifier:observerIdentifier:keyPath:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_observerInstanceId = GetNullableObjectAtIndex(args, 1); + NSString *arg_keyPath = GetNullableObjectAtIndex(args, 2); + FlutterError *error; + [api removeObserverForObjectWithIdentifier:arg_instanceId + observerIdentifier:arg_observerInstanceId + keyPath:arg_keyPath + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFFunctionFlutterApiCodecReader : FlutterStandardReader +@end +@implementation FWFFunctionFlutterApiCodecReader +@end + +@interface FWFFunctionFlutterApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFFunctionFlutterApiCodecWriter +@end + +@interface FWFFunctionFlutterApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFFunctionFlutterApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFFunctionFlutterApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFFunctionFlutterApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFFunctionFlutterApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFFunctionFlutterApiCodecReaderWriter *readerWriter = + [[FWFFunctionFlutterApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +@interface FWFFunctionFlutterApi () +@property(nonatomic, strong) NSObject *binaryMessenger; +@end + +@implementation FWFFunctionFlutterApi + +- (instancetype)initWithBinaryMessenger:(NSObject *)binaryMessenger { + self = [super init]; + if (self) { + _binaryMessenger = binaryMessenger; + } + return self; +} +- (void)disposeFunctionWithIdentifier:(NSNumber *)arg_instanceId + completion:(void (^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FunctionFlutterApi.dispose" + binaryMessenger:self.binaryMessenger + codec:FWFFunctionFlutterApiGetCodec()]; + [channel sendMessage:@[ (arg_instanceId == nil) ? [NSNull null] : arg_instanceId ] + reply:^(id reply) { + completion(nil); + }]; +} +@end +@interface FWFWKWebViewHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKWebViewHostApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FWFNSHttpCookieData fromMap:[self readValue]]; + + case 129: + return [FWFNSHttpCookiePropertyKeyEnumData fromMap:[self readValue]]; + + case 130: + return [FWFNSKeyValueObservingOptionsEnumData fromMap:[self readValue]]; + + case 131: + return [FWFNSUrlRequestData fromMap:[self readValue]]; + + case 132: + return [FWFWKAudiovisualMediaTypeEnumData fromMap:[self readValue]]; + + case 133: + return [FWFWKUserScriptData fromMap:[self readValue]]; + + case 134: + return [FWFWKUserScriptInjectionTimeEnumData fromMap:[self readValue]]; + + case 135: + return [FWFWKWebsiteDataTypesEnumData fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FWFWKWebViewHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKWebViewHostApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FWFNSHttpCookieData class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFNSHttpCookiePropertyKeyEnumData class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFNSKeyValueObservingOptionsEnumData class]]) { + [self writeByte:130]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFNSUrlRequestData class]]) { + [self writeByte:131]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKAudiovisualMediaTypeEnumData class]]) { + [self writeByte:132]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKUserScriptData class]]) { + [self writeByte:133]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKUserScriptInjectionTimeEnumData class]]) { + [self writeByte:134]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKWebsiteDataTypesEnumData class]]) { + [self writeByte:135]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FWFWKWebViewHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKWebViewHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKWebViewHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKWebViewHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKWebViewHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKWebViewHostApiCodecReaderWriter *readerWriter = + [[FWFWKWebViewHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKWebViewHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.create" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createWithIdentifier: + configurationIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(createWithIdentifier:configurationIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_configurationInstanceId = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api createWithIdentifier:arg_instanceId + configurationIdentifier:arg_configurationInstanceId + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.setUIDelegate" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setUIDelegateForWebViewWithIdentifier: + delegateIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(setUIDelegateForWebViewWithIdentifier:delegateIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_uiDelegateInstanceId = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setUIDelegateForWebViewWithIdentifier:arg_instanceId + delegateIdentifier:arg_uiDelegateInstanceId + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.setNavigationDelegate" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(setNavigationDelegateForWebViewWithIdentifier: + delegateIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(setNavigationDelegateForWebViewWithIdentifier:delegateIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_navigationDelegateInstanceId = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setNavigationDelegateForWebViewWithIdentifier:arg_instanceId + delegateIdentifier:arg_navigationDelegateInstanceId + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.getUrl" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(URLForWebViewWithIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(URLForWebViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + NSString *output = [api URLForWebViewWithIdentifier:arg_instanceId error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.getEstimatedProgress" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(estimatedProgressForWebViewWithIdentifier: + error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(estimatedProgressForWebViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + NSNumber *output = [api estimatedProgressForWebViewWithIdentifier:arg_instanceId + error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.loadRequest" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(loadRequestForWebViewWithIdentifier: + request:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(loadRequestForWebViewWithIdentifier:request:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FWFNSUrlRequestData *arg_request = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api loadRequestForWebViewWithIdentifier:arg_instanceId request:arg_request error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.loadHtmlString" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(loadHTMLForWebViewWithIdentifier: + HTMLString:baseURL:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(loadHTMLForWebViewWithIdentifier:HTMLString:baseURL:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSString *arg_string = GetNullableObjectAtIndex(args, 1); + NSString *arg_baseUrl = GetNullableObjectAtIndex(args, 2); + FlutterError *error; + [api loadHTMLForWebViewWithIdentifier:arg_instanceId + HTMLString:arg_string + baseURL:arg_baseUrl + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.loadFileUrl" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (loadFileForWebViewWithIdentifier:fileURL:readAccessURL:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(loadFileForWebViewWithIdentifier:fileURL:readAccessURL:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSString *arg_url = GetNullableObjectAtIndex(args, 1); + NSString *arg_readAccessUrl = GetNullableObjectAtIndex(args, 2); + FlutterError *error; + [api loadFileForWebViewWithIdentifier:arg_instanceId + fileURL:arg_url + readAccessURL:arg_readAccessUrl + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.loadFlutterAsset" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(loadAssetForWebViewWithIdentifier: + assetKey:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(loadAssetForWebViewWithIdentifier:assetKey:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSString *arg_key = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api loadAssetForWebViewWithIdentifier:arg_instanceId assetKey:arg_key error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.canGoBack" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(canGoBackForWebViewWithIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(canGoBackForWebViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + NSNumber *output = [api canGoBackForWebViewWithIdentifier:arg_instanceId error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.canGoForward" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(canGoForwardForWebViewWithIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(canGoForwardForWebViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + NSNumber *output = [api canGoForwardForWebViewWithIdentifier:arg_instanceId error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.goBack" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(goBackForWebViewWithIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(goBackForWebViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api goBackForWebViewWithIdentifier:arg_instanceId error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.goForward" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(goForwardForWebViewWithIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(goForwardForWebViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api goForwardForWebViewWithIdentifier:arg_instanceId error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.reload" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(reloadWebViewWithIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(reloadWebViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api reloadWebViewWithIdentifier:arg_instanceId error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.getTitle" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(titleForWebViewWithIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(titleForWebViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + NSString *output = [api titleForWebViewWithIdentifier:arg_instanceId error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + @"dev.flutter.pigeon.WKWebViewHostApi.setAllowsBackForwardNavigationGestures" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (setAllowsBackForwardForWebViewWithIdentifier:isAllowed:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(setAllowsBackForwardForWebViewWithIdentifier:isAllowed:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_allow = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setAllowsBackForwardForWebViewWithIdentifier:arg_instanceId + isAllowed:arg_allow + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.setCustomUserAgent" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setUserAgentForWebViewWithIdentifier: + userAgent:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(setUserAgentForWebViewWithIdentifier:userAgent:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSString *arg_userAgent = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setUserAgentForWebViewWithIdentifier:arg_instanceId + userAgent:arg_userAgent + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.evaluateJavaScript" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector + (evaluateJavaScriptForWebViewWithIdentifier:javaScriptString:completion:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(evaluateJavaScriptForWebViewWithIdentifier:javaScriptString:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSString *arg_javaScriptString = GetNullableObjectAtIndex(args, 1); + [api evaluateJavaScriptForWebViewWithIdentifier:arg_instanceId + javaScriptString:arg_javaScriptString + completion:^(id _Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFWKUIDelegateHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKUIDelegateHostApiCodecReader +@end + +@interface FWFWKUIDelegateHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKUIDelegateHostApiCodecWriter +@end + +@interface FWFWKUIDelegateHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKUIDelegateHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKUIDelegateHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKUIDelegateHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKUIDelegateHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKUIDelegateHostApiCodecReaderWriter *readerWriter = + [[FWFWKUIDelegateHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKUIDelegateHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKUIDelegateHostApi.create" + binaryMessenger:binaryMessenger + codec:FWFWKUIDelegateHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createWithIdentifier:error:)], + @"FWFWKUIDelegateHostApi api (%@) doesn't respond to " + @"@selector(createWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api createWithIdentifier:arg_instanceId error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFWKHttpCookieStoreHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKHttpCookieStoreHostApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FWFNSHttpCookieData fromMap:[self readValue]]; + + case 129: + return [FWFNSHttpCookiePropertyKeyEnumData fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FWFWKHttpCookieStoreHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKHttpCookieStoreHostApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FWFNSHttpCookieData class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFNSHttpCookiePropertyKeyEnumData class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FWFWKHttpCookieStoreHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKHttpCookieStoreHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKHttpCookieStoreHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKHttpCookieStoreHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKHttpCookieStoreHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKHttpCookieStoreHostApiCodecReaderWriter *readerWriter = + [[FWFWKHttpCookieStoreHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKHttpCookieStoreHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKHttpCookieStoreHostApi.createFromWebsiteDataStore" + binaryMessenger:binaryMessenger + codec:FWFWKHttpCookieStoreHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createFromWebsiteDataStoreWithIdentifier: + dataStoreIdentifier:error:)], + @"FWFWKHttpCookieStoreHostApi api (%@) doesn't respond to " + @"@selector(createFromWebsiteDataStoreWithIdentifier:dataStoreIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_websiteDataStoreInstanceId = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api createFromWebsiteDataStoreWithIdentifier:arg_instanceId + dataStoreIdentifier:arg_websiteDataStoreInstanceId + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKHttpCookieStoreHostApi.setCookie" + binaryMessenger:binaryMessenger + codec:FWFWKHttpCookieStoreHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setCookieForStoreWithIdentifier:cookie:error:)], + @"FWFWKHttpCookieStoreHostApi api (%@) doesn't respond to " + @"@selector(setCookieForStoreWithIdentifier:cookie:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_instanceId = GetNullableObjectAtIndex(args, 0); + FWFNSHttpCookieData *arg_cookie = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setCookieForStoreWithIdentifier:arg_instanceId cookie:arg_cookie error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFInstanceManager.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFInstanceManager.h new file mode 100644 index 000000000000..79506bf72adc --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFInstanceManager.h @@ -0,0 +1,70 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Maintains instances to intercommunicate with Dart objects. + * + * When an instance is added with an identifier, either can be used to retrieve the other. + */ +@interface FWFInstanceManager : NSObject +// TODO(bparrishMines): Pairs should not be able to be overwritten and this feature +// should be replaced with a call to clear the manager in the event of a hot restart +// instead. +/** + * Adds a new instance to the manager. + * + * If an instance or identifier has already been added, it will be replaced by the new values. The + * Dart InstanceManager is considered the source of truth and has the capability to overwrite stored + * pairs in response to hot restarts. + * + * @param instance The instance to be stored. + * @param instanceIdentifier The identifier to be paired with instance. This value must be >= 0. + */ +- (void)addInstance:(NSObject *)instance withIdentifier:(long)instanceIdentifier; + +/** + * Removes the instance paired with a given identifier from the manager. + * + * @param instanceIdentifier The identifier paired to an instance. + * + * @return The removed instance if the manager contains the given instanceIdentifier, otherwise + * nil. + */ +- (nullable NSObject *)removeInstanceWithIdentifier:(long)instanceIdentifier; + +/** + * Removes the instance from the manager. + * + * @param instance The instance to be removed from the manager. + * + * @return The identifier of the removed instance if the manager contains the given instance, + * otherwise NSNotFound. + */ +- (long)removeInstance:(NSObject *)instance; + +/** + * Retrieves the instance paired with identifier. + * + * @param instanceIdentifier The identifier paired to an instance. + * + * @return The paired instance if the manager contains the given instanceIdentifier, + * otherwise nil. + */ +- (nullable NSObject *)instanceForIdentifier:(long)instanceIdentifier; + +/** + * Retrieves the identifier paired with an instance. + * + * @param instance An instance that may be stored in the manager. + * + * @return The paired identifer if the manager contains the given instance, otherwise NSNotFound. + */ +- (long)identifierForInstance:(NSObject *)instance; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFInstanceManager.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFInstanceManager.m new file mode 100644 index 000000000000..445ecd3e2cf9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFInstanceManager.m @@ -0,0 +1,72 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FWFInstanceManager.h" + +@interface FWFInstanceManager () +@property dispatch_queue_t lockQueue; +@property NSMapTable *identifiersToInstances; +@property NSMapTable *instancesToIdentifiers; +@end + +@implementation FWFInstanceManager +- (instancetype)init { + if (self) { + _lockQueue = dispatch_queue_create("FWFInstanceManager", DISPATCH_QUEUE_SERIAL); + _identifiersToInstances = [NSMapTable strongToStrongObjectsMapTable]; + _instancesToIdentifiers = [NSMapTable strongToStrongObjectsMapTable]; + } + return self; +} + +- (void)addInstance:(nonnull NSObject *)instance withIdentifier:(long)instanceIdentifier { + NSAssert(instance && instanceIdentifier >= 0, + @"Instance must be nonnull and identifier must be >= 0."); + dispatch_async(_lockQueue, ^{ + [self.instancesToIdentifiers setObject:@(instanceIdentifier) forKey:instance]; + [self.identifiersToInstances setObject:instance forKey:@(instanceIdentifier)]; + }); +} + +- (nullable NSObject *)removeInstanceWithIdentifier:(long)instanceIdentifier { + NSObject *__block instance = nil; + dispatch_sync(_lockQueue, ^{ + instance = [self.identifiersToInstances objectForKey:@(instanceIdentifier)]; + if (instance) { + [self.identifiersToInstances removeObjectForKey:@(instanceIdentifier)]; + [self.instancesToIdentifiers removeObjectForKey:instance]; + } + }); + return instance; +} + +- (long)removeInstance:(NSObject *)instance { + NSAssert(instance, @"Instance must be nonnull."); + NSNumber *__block identifierNumber = nil; + dispatch_sync(_lockQueue, ^{ + identifierNumber = [self.instancesToIdentifiers objectForKey:instance]; + if (identifierNumber) { + [self.identifiersToInstances removeObjectForKey:identifierNumber]; + [self.instancesToIdentifiers removeObjectForKey:instance]; + } + }); + return identifierNumber ? identifierNumber.longValue : NSNotFound; +} + +- (nullable NSObject *)instanceForIdentifier:(long)instanceIdentifier { + NSObject *__block instance = nil; + dispatch_sync(_lockQueue, ^{ + instance = [self.identifiersToInstances objectForKey:@(instanceIdentifier)]; + }); + return instance; +} + +- (long)identifierForInstance:(nonnull NSObject *)instance { + NSNumber *__block identifierNumber = nil; + dispatch_sync(_lockQueue, ^{ + identifierNumber = [self.instancesToIdentifiers objectForKey:instance]; + }); + return identifierNumber ? identifierNumber.longValue : NSNotFound; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.h new file mode 100644 index 000000000000..968a9a85b2e7 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.h @@ -0,0 +1,40 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +#import "FWFGeneratedWebKitApis.h" +#import "FWFInstanceManager.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * A set of Flutter and Dart assets used by a `FlutterEngine` to initialize execution. + * + * Default implementation delegates methods to FlutterDartProject. + */ +@interface FWFAssetManager : NSObject +- (NSString *)lookupKeyForAsset:(NSString *)asset; +@end + +/** + * Implementation of WKWebView that can be used as a FlutterPlatformView. + */ +@interface FWFWebView : WKWebView +@end + +/** + * Host api implementation for WKWebView. + * + * Handles creating WKWebViews that intercommunicate with a paired Dart object. + */ +@interface FWFWebViewHostApiImpl : NSObject +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager; +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager + bundle:(NSBundle *)bundle + assetManager:(FWFAssetManager *)assetManager; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.m new file mode 100644 index 000000000000..35677575dddb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.m @@ -0,0 +1,249 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FWFWebViewHostApi.h" +#import "FWFDataConverters.h" + +@implementation FWFAssetManager +- (NSString *)lookupKeyForAsset:(NSString *)asset { + return [FlutterDartProject lookupKeyForAsset:asset]; +} +@end + +@implementation FWFWebView +- (void)setFrame:(CGRect)frame { + [super setFrame:frame]; + // Prevents the contentInsets from being adjusted by iOS and gives control to Flutter. + self.scrollView.contentInset = UIEdgeInsetsZero; + if (@available(iOS 11, *)) { + // Above iOS 11, adjust contentInset to compensate the adjustedContentInset so the sum will + // always be 0. + if (UIEdgeInsetsEqualToEdgeInsets(self.scrollView.adjustedContentInset, UIEdgeInsetsZero)) { + return; + } + UIEdgeInsets insetToAdjust = self.scrollView.adjustedContentInset; + self.scrollView.contentInset = UIEdgeInsetsMake(-insetToAdjust.top, -insetToAdjust.left, + -insetToAdjust.bottom, -insetToAdjust.right); + } +} + +- (nonnull UIView *)view { + return self; +} +@end + +@interface FWFWebViewHostApiImpl () +@property(nonatomic) FWFInstanceManager *instanceManager; +@property NSBundle *bundle; +@property FWFAssetManager *assetManager; +@end + +@implementation FWFWebViewHostApiImpl +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager { + return [self initWithInstanceManager:instanceManager + bundle:[NSBundle mainBundle] + assetManager:[[FWFAssetManager alloc] init]]; +} + +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager + bundle:(NSBundle *)bundle + assetManager:(FWFAssetManager *)assetManager { + self = [self init]; + if (self) { + _instanceManager = instanceManager; + _bundle = bundle; + _assetManager = assetManager; + } + return self; +} + +- (FWFWebView *)webViewForIdentifier:(NSNumber *)instanceId { + return (FWFWebView *)[self.instanceManager instanceForIdentifier:instanceId.longValue]; +} + ++ (nonnull FlutterError *)errorForURLString:(nonnull NSString *)string { + NSString *errorDetails = [NSString stringWithFormat:@"Initializing NSURL with the supplied " + @"'%@' path resulted in a nil value.", + string]; + return [FlutterError errorWithCode:@"FWFURLParsingError" + message:@"Failed parsing file path." + details:errorDetails]; +} + +- (void)createWithIdentifier:(nonnull NSNumber *)instanceId + configurationIdentifier:(nonnull NSNumber *)configurationInstanceId + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + WKWebViewConfiguration *configuration = (WKWebViewConfiguration *)[self.instanceManager + instanceForIdentifier:configurationInstanceId.longValue]; + FWFWebView *webView = [[FWFWebView alloc] initWithFrame:CGRectMake(0, 0, 0, 0) + configuration:configuration]; + [self.instanceManager addInstance:webView withIdentifier:instanceId.longValue]; +} + +- (void)loadRequestForWebViewWithIdentifier:(nonnull NSNumber *)instanceId + request:(nonnull FWFNSUrlRequestData *)request + error: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { + NSURLRequest *urlRequest = FWFNSURLRequestFromRequestData(request); + if (!urlRequest) { + *error = [FlutterError errorWithCode:@"FWFURLRequestParsingError" + message:@"Failed instantiating an NSURLRequest." + details:[NSString stringWithFormat:@"URL was: '%@'", request.url]]; + return; + } + [[self webViewForIdentifier:instanceId] loadRequest:urlRequest]; +} + +- (void)setUserAgentForWebViewWithIdentifier:(nonnull NSNumber *)instanceId + userAgent:(nullable NSString *)userAgent + error:(FlutterError *_Nullable __autoreleasing *_Nonnull) + error { + [[self webViewForIdentifier:instanceId] setCustomUserAgent:userAgent]; +} + +- (nullable NSNumber *) + canGoBackForWebViewWithIdentifier:(nonnull NSNumber *)instanceId + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + return @([self webViewForIdentifier:instanceId].canGoBack); +} + +- (nullable NSString *) + URLForWebViewWithIdentifier:(nonnull NSNumber *)instanceId + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + return [self webViewForIdentifier:instanceId].URL.absoluteString; +} + +- (nullable NSNumber *) + canGoForwardForWebViewWithIdentifier:(nonnull NSNumber *)instanceId + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + return @([[self webViewForIdentifier:instanceId] canGoForward]); +} + +- (nullable NSNumber *) + estimatedProgressForWebViewWithIdentifier:(nonnull NSNumber *)instanceId + error:(FlutterError *_Nullable __autoreleasing *_Nonnull) + error { + return @([[self webViewForIdentifier:instanceId] estimatedProgress]); +} + +- (void)evaluateJavaScriptForWebViewWithIdentifier:(nonnull NSNumber *)instanceId + javaScriptString:(nonnull NSString *)javaScriptString + completion: + (nonnull void (^)(id _Nullable, + FlutterError *_Nullable))completion { + [[self webViewForIdentifier:instanceId] + evaluateJavaScript:javaScriptString + completionHandler:^(id _Nullable result, NSError *_Nullable error) { + id returnValue = nil; + FlutterError *flutterError = nil; + if (!error) { + if (!result || [result isKindOfClass:[NSString class]] || + [result isKindOfClass:[NSNumber class]]) { + returnValue = result; + } else { + NSString *className = NSStringFromClass([result class]); + NSLog(@"Return type of evaluateJavaScript is not directly supported: %@. Returned " + @"description of value.", + className); + returnValue = [result description]; + } + } else { + flutterError = [FlutterError errorWithCode:@"FWFEvaluateJavaScriptError" + message:@"Failed evaluating JavaScript." + details:error]; + } + + completion(returnValue, flutterError); + }]; +} + +- (void)goBackForWebViewWithIdentifier:(nonnull NSNumber *)instanceId + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + [[self webViewForIdentifier:instanceId] goBack]; +} + +- (void)goForwardForWebViewWithIdentifier:(nonnull NSNumber *)instanceId + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + [[self webViewForIdentifier:instanceId] goForward]; +} + +- (void)loadAssetForWebViewWithIdentifier:(nonnull NSNumber *)instanceId + assetKey:(nonnull NSString *)key + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + NSString *assetFilePath = [self.assetManager lookupKeyForAsset:key]; + + NSURL *url = [self.bundle URLForResource:[assetFilePath stringByDeletingPathExtension] + withExtension:assetFilePath.pathExtension]; + if (!url) { + *error = [FWFWebViewHostApiImpl errorForURLString:assetFilePath]; + } else { + [[self webViewForIdentifier:instanceId] loadFileURL:url + allowingReadAccessToURL:[url URLByDeletingLastPathComponent]]; + } +} + +- (void)loadFileForWebViewWithIdentifier:(nonnull NSNumber *)instanceId + fileURL:(nonnull NSString *)url + readAccessURL:(nonnull NSString *)readAccessUrl + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + NSURL *fileURL = [NSURL fileURLWithPath:url isDirectory:NO]; + NSURL *readAccessNSURL = [NSURL fileURLWithPath:readAccessUrl isDirectory:YES]; + + if (!fileURL) { + *error = [FWFWebViewHostApiImpl errorForURLString:url]; + } else if (!readAccessNSURL) { + *error = [FWFWebViewHostApiImpl errorForURLString:readAccessUrl]; + } else { + [[self webViewForIdentifier:instanceId] loadFileURL:fileURL + allowingReadAccessToURL:readAccessNSURL]; + } +} + +- (void)loadHTMLForWebViewWithIdentifier:(nonnull NSNumber *)instanceId + HTMLString:(nonnull NSString *)string + baseURL:(nullable NSString *)baseUrl + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + [[self webViewForIdentifier:instanceId] loadHTMLString:string + baseURL:[NSURL URLWithString:baseUrl]]; +} + +- (void)reloadWebViewWithIdentifier:(nonnull NSNumber *)instanceId + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + [[self webViewForIdentifier:instanceId] reload]; +} + +- (void) + setAllowsBackForwardForWebViewWithIdentifier:(nonnull NSNumber *)instanceId + isAllowed:(nonnull NSNumber *)allow + error:(FlutterError *_Nullable __autoreleasing *_Nonnull) + error { + [[self webViewForIdentifier:instanceId] setAllowsBackForwardNavigationGestures:allow.boolValue]; +} + +- (void) + setNavigationDelegateForWebViewWithIdentifier:(nonnull NSNumber *)instanceId + delegateIdentifier:(nullable NSNumber *)navigationDelegateInstanceId + error: + (FlutterError *_Nullable __autoreleasing *_Nonnull) + error { + id navigationDelegate = (id)[self.instanceManager + instanceForIdentifier:navigationDelegateInstanceId.longValue]; + [[self webViewForIdentifier:instanceId] setNavigationDelegate:navigationDelegate]; +} + +- (void)setUIDelegateForWebViewWithIdentifier:(nonnull NSNumber *)instanceId + delegateIdentifier:(nullable NSNumber *)uiDelegateInstanceId + error:(FlutterError *_Nullable __autoreleasing *_Nonnull) + error { + id navigationDelegate = + (id)[self.instanceManager instanceForIdentifier:uiDelegateInstanceId.longValue]; + [[self webViewForIdentifier:instanceId] setUIDelegate:navigationDelegate]; +} + +- (nullable NSString *) + titleForWebViewWithIdentifier:(nonnull NSNumber *)instanceId + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + return [[self webViewForIdentifier:instanceId] title]; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m index 61f112019686..5bb81fce89db 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m @@ -449,19 +449,14 @@ - (void)onRemoveJavaScriptChannels:(FlutterMethodCall *)call result:(FlutterResu } - (void)clearCache:(FlutterResult)result { - if (@available(iOS 9.0, *)) { - NSSet *cacheDataTypes = [WKWebsiteDataStore allWebsiteDataTypes]; - WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore]; - NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0]; - [dataStore removeDataOfTypes:cacheDataTypes - modifiedSince:dateFrom - completionHandler:^{ - result(nil); - }]; - } else { - // support for iOS8 tracked in https://github.com/flutter/flutter/issues/27624. - NSLog(@"Clearing cache is not supported for Flutter WebViews prior to iOS 9."); - } + NSSet *cacheDataTypes = [WKWebsiteDataStore allWebsiteDataTypes]; + WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore]; + NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0]; + [dataStore removeDataOfTypes:cacheDataTypes + modifiedSince:dateFrom + completionHandler:^{ + result(nil); + }]; } - (void)onGetTitle:(FlutterResult)result { @@ -571,24 +566,20 @@ - (void)updateAutoMediaPlaybackPolicy:(NSNumber *)policy case 0: // require_user_action_for_all_media_types if (@available(iOS 10.0, *)) { configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; - } else if (@available(iOS 9.0, *)) { - configuration.requiresUserActionForMediaPlayback = true; } else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - configuration.mediaPlaybackRequiresUserAction = true; + configuration.requiresUserActionForMediaPlayback = true; #pragma clang diagnostic pop } break; case 1: // always_allow if (@available(iOS 10.0, *)) { configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; - } else if (@available(iOS 9.0, *)) { - configuration.requiresUserActionForMediaPlayback = false; } else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - configuration.mediaPlaybackRequiresUserAction = false; + configuration.requiresUserActionForMediaPlayback = false; #pragma clang diagnostic pop } break; @@ -658,11 +649,7 @@ - (void)registerJavaScriptChannels:(NSSet *)channelNames } - (void)updateUserAgent:(NSString *)userAgent { - if (@available(iOS 9.0, *)) { - [_webView setCustomUserAgent:userAgent]; - } else { - NSLog(@"Updating UserAgent is not supported for Flutter WebViews prior to iOS 9."); - } + [_webView setCustomUserAgent:userAgent]; } /** diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h index c37282e886bc..9a7fd1b5ef3f 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h @@ -7,5 +7,9 @@ #import #import #import +#import +#import +#import +#import #import #import diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/function_flutter_api_impls.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/function_flutter_api_impls.dart new file mode 100644 index 000000000000..c6eb711513d2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/function_flutter_api_impls.dart @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'instance_manager.dart'; +import 'web_kit.pigeon.dart'; + +/// Flutter api to dispose functions. +class FunctionFlutterApiImpl extends FunctionFlutterApi { + /// Constructs a [FunctionFlutterApiImpl]. + FunctionFlutterApiImpl({InstanceManager? instanceManager}) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with native language objects. + late final InstanceManager instanceManager; + + @override + void dispose(int instanceId) { + final Function? function = instanceManager.getInstance(instanceId); + if (function != null) { + instanceManager.removeInstance(function); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/instance_manager.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/instance_manager.dart new file mode 100644 index 000000000000..830ba2e94935 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/instance_manager.dart @@ -0,0 +1,52 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Maintains instances stored to communicate with Objective-C objects. +class InstanceManager { + final Map _instanceIdsToInstances = {}; + final Map _instancesToInstanceIds = {}; + + int _nextInstanceId = 0; + + /// Global instance of [InstanceManager]. + static final InstanceManager instance = InstanceManager(); + + /// Attempt to add a new instance. + /// + /// Returns new if [instance] has already been added. Otherwise, it is added + /// with a new instance id. + int? tryAddInstance(Object instance) { + if (_instancesToInstanceIds.containsKey(instance)) { + return null; + } + + final int instanceId = _nextInstanceId++; + _instancesToInstanceIds[instance] = instanceId; + _instanceIdsToInstances[instanceId] = instance; + return instanceId; + } + + /// Remove the instance from the manager. + /// + /// Returns null if the instance is removed. Otherwise, return the instanceId + /// of the removed instance. + int? removeInstance(T instance) { + final int? instanceId = _instancesToInstanceIds[instance]; + if (instanceId != null) { + _instancesToInstanceIds.remove(instance); + _instanceIdsToInstances.remove(instanceId); + } + return instanceId; + } + + /// Retrieve the Object paired with instanceId. + T? getInstance(int instanceId) { + return _instanceIdsToInstances[instanceId] as T?; + } + + /// Retrieve the instanceId paired with instance. + int? getInstanceId(Object instance) { + return _instancesToInstanceIds[instance]; + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.pigeon.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.pigeon.dart new file mode 100644 index 000000000000..fa52b43c396e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.pigeon.dart @@ -0,0 +1,2004 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.0.3), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name +// @dart = 2.12 +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; + +import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; + +enum NSKeyValueObservingOptionsEnum { + newValue, + oldValue, + initialValue, + priorNotification, +} + +enum NSKeyValueChangeEnum { + setting, + insertion, + removal, + replacement, +} + +enum NSKeyValueChangeKeyEnum { + indexes, + kind, + newValue, + notificationIsPrior, + oldValue, +} + +enum WKUserScriptInjectionTimeEnum { + atDocumentStart, + atDocumentEnd, +} + +enum WKAudiovisualMediaTypeEnum { + none, + audio, + video, + all, +} + +enum WKWebsiteDataTypesEnum { + cookies, + memoryCache, + diskCache, + offlineWebApplicationCache, + localStroage, + sessionStorage, + sqlDatabases, + indexedDBDatabases, +} + +enum WKNavigationActionPolicyEnum { + allow, + cancel, +} + +enum NSHttpCookiePropertyKeyEnum { + comment, + commentUrl, + discard, + domain, + expires, + maximumAge, + name, + originUrl, + path, + port, + sameSitePolicy, + secure, + value, + version, +} + +class NSKeyValueObservingOptionsEnumData { + NSKeyValueObservingOptionsEnumData({ + this.value, + }); + + NSKeyValueObservingOptionsEnum? value; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['value'] = value?.index; + return pigeonMap; + } + + static NSKeyValueObservingOptionsEnumData decode(Object message) { + final Map pigeonMap = message as Map; + return NSKeyValueObservingOptionsEnumData( + value: pigeonMap['value'] != null + ? NSKeyValueObservingOptionsEnum.values[pigeonMap['value']! as int] + : null, + ); + } +} + +class WKUserScriptInjectionTimeEnumData { + WKUserScriptInjectionTimeEnumData({ + this.value, + }); + + WKUserScriptInjectionTimeEnum? value; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['value'] = value?.index; + return pigeonMap; + } + + static WKUserScriptInjectionTimeEnumData decode(Object message) { + final Map pigeonMap = message as Map; + return WKUserScriptInjectionTimeEnumData( + value: pigeonMap['value'] != null + ? WKUserScriptInjectionTimeEnum.values[pigeonMap['value']! as int] + : null, + ); + } +} + +class WKAudiovisualMediaTypeEnumData { + WKAudiovisualMediaTypeEnumData({ + this.value, + }); + + WKAudiovisualMediaTypeEnum? value; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['value'] = value?.index; + return pigeonMap; + } + + static WKAudiovisualMediaTypeEnumData decode(Object message) { + final Map pigeonMap = message as Map; + return WKAudiovisualMediaTypeEnumData( + value: pigeonMap['value'] != null + ? WKAudiovisualMediaTypeEnum.values[pigeonMap['value']! as int] + : null, + ); + } +} + +class WKWebsiteDataTypesEnumData { + WKWebsiteDataTypesEnumData({ + this.value, + }); + + WKWebsiteDataTypesEnum? value; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['value'] = value?.index; + return pigeonMap; + } + + static WKWebsiteDataTypesEnumData decode(Object message) { + final Map pigeonMap = message as Map; + return WKWebsiteDataTypesEnumData( + value: pigeonMap['value'] != null + ? WKWebsiteDataTypesEnum.values[pigeonMap['value']! as int] + : null, + ); + } +} + +class NSHttpCookiePropertyKeyEnumData { + NSHttpCookiePropertyKeyEnumData({ + this.value, + }); + + NSHttpCookiePropertyKeyEnum? value; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['value'] = value?.index; + return pigeonMap; + } + + static NSHttpCookiePropertyKeyEnumData decode(Object message) { + final Map pigeonMap = message as Map; + return NSHttpCookiePropertyKeyEnumData( + value: pigeonMap['value'] != null + ? NSHttpCookiePropertyKeyEnum.values[pigeonMap['value']! as int] + : null, + ); + } +} + +class NSUrlRequestData { + NSUrlRequestData({ + required this.url, + this.httpMethod, + this.httpBody, + required this.allHttpHeaderFields, + }); + + String url; + String? httpMethod; + Uint8List? httpBody; + Map allHttpHeaderFields; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['url'] = url; + pigeonMap['httpMethod'] = httpMethod; + pigeonMap['httpBody'] = httpBody; + pigeonMap['allHttpHeaderFields'] = allHttpHeaderFields; + return pigeonMap; + } + + static NSUrlRequestData decode(Object message) { + final Map pigeonMap = message as Map; + return NSUrlRequestData( + url: pigeonMap['url']! as String, + httpMethod: pigeonMap['httpMethod'] as String?, + httpBody: pigeonMap['httpBody'] as Uint8List?, + allHttpHeaderFields: + (pigeonMap['allHttpHeaderFields'] as Map?)! + .cast(), + ); + } +} + +class WKUserScriptData { + WKUserScriptData({ + required this.source, + this.injectionTime, + required this.isMainFrameOnly, + }); + + String source; + WKUserScriptInjectionTimeEnumData? injectionTime; + bool isMainFrameOnly; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['source'] = source; + pigeonMap['injectionTime'] = injectionTime?.encode(); + pigeonMap['isMainFrameOnly'] = isMainFrameOnly; + return pigeonMap; + } + + static WKUserScriptData decode(Object message) { + final Map pigeonMap = message as Map; + return WKUserScriptData( + source: pigeonMap['source']! as String, + injectionTime: pigeonMap['injectionTime'] != null + ? WKUserScriptInjectionTimeEnumData.decode( + pigeonMap['injectionTime']!) + : null, + isMainFrameOnly: pigeonMap['isMainFrameOnly']! as bool, + ); + } +} + +class NSHttpCookieData { + NSHttpCookieData({ + required this.properties, + }); + + Map properties; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['properties'] = properties; + return pigeonMap; + } + + static NSHttpCookieData decode(Object message) { + final Map pigeonMap = message as Map; + return NSHttpCookieData( + properties: (pigeonMap['properties'] as Map?)! + .cast(), + ); + } +} + +class _WKWebsiteDataStoreHostApiCodec extends StandardMessageCodec { + const _WKWebsiteDataStoreHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is WKWebsiteDataTypesEnumData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WKWebsiteDataTypesEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class WKWebsiteDataStoreHostApi { + /// Constructor for [WKWebsiteDataStoreHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + WKWebsiteDataStoreHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WKWebsiteDataStoreHostApiCodec(); + + Future createFromWebViewConfiguration( + int arg_instanceId, int arg_configurationInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createFromWebViewConfiguration', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_configurationInstanceId]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future createDefaultDataStore(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createDefaultDataStore', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future removeDataOfTypes( + int arg_instanceId, + List arg_dataTypes, + double arg_secondsModifiedSinceEpoch) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebsiteDataStoreHostApi.removeDataOfTypes', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel.send([ + arg_instanceId, + arg_dataTypes, + arg_secondsModifiedSinceEpoch + ]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as bool?)!; + } + } +} + +class _UIViewHostApiCodec extends StandardMessageCodec { + const _UIViewHostApiCodec(); +} + +class UIViewHostApi { + /// Constructor for [UIViewHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + UIViewHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _UIViewHostApiCodec(); + + Future> getContentOffset(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIViewHostApi.getContentOffset', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as List?)!.cast(); + } + } + + Future setBackgroundColor(int arg_instanceId, int? arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIViewHostApi.setBackgroundColor', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_value]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setOpaque(int arg_instanceId, bool arg_opaque) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIViewHostApi.setOpaque', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_opaque]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _UIScrollViewHostApiCodec extends StandardMessageCodec { + const _UIScrollViewHostApiCodec(); +} + +class UIScrollViewHostApi { + /// Constructor for [UIScrollViewHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + UIScrollViewHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _UIScrollViewHostApiCodec(); + + Future createFromWebView( + int arg_instanceId, int arg_webViewInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIScrollViewHostApi.createFromWebView', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_webViewInstanceId]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future> getContentOffset(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIScrollViewHostApi.getContentOffset', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as List?)!.cast(); + } + } + + Future scrollBy(int arg_instanceId, double arg_x, double arg_y) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIScrollViewHostApi.scrollBy', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_x, arg_y]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setContentOffset( + int arg_instanceId, double arg_x, double arg_y) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIScrollViewHostApi.setContentOffset', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_x, arg_y]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _WKWebViewConfigurationHostApiCodec extends StandardMessageCodec { + const _WKWebViewConfigurationHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is WKAudiovisualMediaTypeEnumData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WKAudiovisualMediaTypeEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class WKWebViewConfigurationHostApi { + /// Constructor for [WKWebViewConfigurationHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + WKWebViewConfigurationHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = + _WKWebViewConfigurationHostApiCodec(); + + Future create(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future createFromWebView( + int arg_instanceId, int arg_webViewInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.createFromWebView', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_webViewInstanceId]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setAllowsInlineMediaPlayback( + int arg_instanceId, bool arg_allow) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.setAllowsInlineMediaPlayback', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_allow]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setMediaTypesRequiringUserActionForPlayback(int arg_instanceId, + List arg_types) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.setMediaTypesRequiringUserActionForPlayback', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_types]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _WKUserContentControllerHostApiCodec extends StandardMessageCodec { + const _WKUserContentControllerHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is WKUserScriptData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptInjectionTimeEnumData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WKUserScriptData.decode(readValue(buffer)!); + + case 129: + return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class WKUserContentControllerHostApi { + /// Constructor for [WKUserContentControllerHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + WKUserContentControllerHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = + _WKUserContentControllerHostApiCodec(); + + Future createFromWebViewConfiguration( + int arg_instanceId, int arg_configurationInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.createFromWebViewConfiguration', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_configurationInstanceId]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future addScriptMessageHandler( + int arg_instanceId, int arg_handlerInstanceid, String arg_name) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.addScriptMessageHandler', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_handlerInstanceid, arg_name]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future removeScriptMessageHandler( + int arg_instanceId, String arg_name) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.removeScriptMessageHandler', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_name]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future removeAllScriptMessageHandlers(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllScriptMessageHandlers', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future addUserScript( + int arg_instanceId, WKUserScriptData arg_userScript) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.addUserScript', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_userScript]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future removeAllUserScripts(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllUserScripts', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _WKPreferencesHostApiCodec extends StandardMessageCodec { + const _WKPreferencesHostApiCodec(); +} + +class WKPreferencesHostApi { + /// Constructor for [WKPreferencesHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + WKPreferencesHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WKPreferencesHostApiCodec(); + + Future createFromWebViewConfiguration( + int arg_instanceId, int arg_configurationInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKPreferencesHostApi.createFromWebViewConfiguration', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_configurationInstanceId]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setJavaScriptEnabled( + int arg_instanceId, bool arg_enabled) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKPreferencesHostApi.setJavaScriptEnabled', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_enabled]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _WKScriptMessageHandlerHostApiCodec extends StandardMessageCodec { + const _WKScriptMessageHandlerHostApiCodec(); +} + +class WKScriptMessageHandlerHostApi { + /// Constructor for [WKScriptMessageHandlerHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + WKScriptMessageHandlerHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = + _WKScriptMessageHandlerHostApiCodec(); + + Future create(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKScriptMessageHandlerHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _WKNavigationDelegateHostApiCodec extends StandardMessageCodec { + const _WKNavigationDelegateHostApiCodec(); +} + +class WKNavigationDelegateHostApi { + /// Constructor for [WKNavigationDelegateHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + WKNavigationDelegateHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = + _WKNavigationDelegateHostApiCodec(); + + Future create(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKNavigationDelegateHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setDidFinishNavigation( + int arg_instanceId, int? arg_functionInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKNavigationDelegateHostApi.setDidFinishNavigation', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_functionInstanceId]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _WKNavigationDelegateFlutterApiCodec extends StandardMessageCodec { + const _WKNavigationDelegateFlutterApiCodec(); +} + +abstract class WKNavigationDelegateFlutterApi { + static const MessageCodec codec = + _WKNavigationDelegateFlutterApiCodec(); + + void didFinishNavigation( + int functionInstanceId, int webViewInstanceId, String? url); + static void setup(WKNavigationDelegateFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFinishNavigation', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFinishNavigation was null.'); + final List args = (message as List?)!; + final int? arg_functionInstanceId = (args[0] as int?); + assert(arg_functionInstanceId != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFinishNavigation was null, expected non-null int.'); + final int? arg_webViewInstanceId = (args[1] as int?); + assert(arg_webViewInstanceId != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFinishNavigation was null, expected non-null int.'); + final String? arg_url = (args[2] as String?); + api.didFinishNavigation( + arg_functionInstanceId!, arg_webViewInstanceId!, arg_url); + return; + }); + } + } + } +} + +class _NSObjectHostApiCodec extends StandardMessageCodec { + const _NSObjectHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NSKeyValueObservingOptionsEnumData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NSKeyValueObservingOptionsEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class NSObjectHostApi { + /// Constructor for [NSObjectHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + NSObjectHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _NSObjectHostApiCodec(); + + Future dispose(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NSObjectHostApi.dispose', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future addObserver( + int arg_instanceId, + int arg_observerInstanceId, + String arg_keyPath, + List arg_options) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NSObjectHostApi.addObserver', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel.send([ + arg_instanceId, + arg_observerInstanceId, + arg_keyPath, + arg_options + ]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future removeObserver(int arg_instanceId, int arg_observerInstanceId, + String arg_keyPath) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NSObjectHostApi.removeObserver', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel.send( + [arg_instanceId, arg_observerInstanceId, arg_keyPath]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _FunctionFlutterApiCodec extends StandardMessageCodec { + const _FunctionFlutterApiCodec(); +} + +abstract class FunctionFlutterApi { + static const MessageCodec codec = _FunctionFlutterApiCodec(); + + void dispose(int instanceId); + static void setup(FunctionFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FunctionFlutterApi.dispose', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.FunctionFlutterApi.dispose was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.FunctionFlutterApi.dispose was null, expected non-null int.'); + api.dispose(arg_instanceId!); + return; + }); + } + } + } +} + +class _WKWebViewHostApiCodec extends StandardMessageCodec { + const _WKWebViewHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NSHttpCookieData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is NSHttpCookiePropertyKeyEnumData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is NSKeyValueObservingOptionsEnumData) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is NSUrlRequestData) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is WKAudiovisualMediaTypeEnumData) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptData) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptInjectionTimeEnumData) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is WKWebsiteDataTypesEnumData) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NSHttpCookieData.decode(readValue(buffer)!); + + case 129: + return NSHttpCookiePropertyKeyEnumData.decode(readValue(buffer)!); + + case 130: + return NSKeyValueObservingOptionsEnumData.decode(readValue(buffer)!); + + case 131: + return NSUrlRequestData.decode(readValue(buffer)!); + + case 132: + return WKAudiovisualMediaTypeEnumData.decode(readValue(buffer)!); + + case 133: + return WKUserScriptData.decode(readValue(buffer)!); + + case 134: + return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + + case 135: + return WKWebsiteDataTypesEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class WKWebViewHostApi { + /// Constructor for [WKWebViewHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + WKWebViewHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WKWebViewHostApiCodec(); + + Future create( + int arg_instanceId, int arg_configurationInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_configurationInstanceId]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setUIDelegate( + int arg_instanceId, int? arg_uiDelegateInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.setUIDelegate', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_uiDelegateInstanceId]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setNavigationDelegate( + int arg_instanceId, int? arg_navigationDelegateInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.setNavigationDelegate', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_navigationDelegateInstanceId]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future getUrl(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.getUrl', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as String?); + } + } + + Future getEstimatedProgress(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.getEstimatedProgress', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as double?)!; + } + } + + Future loadRequest( + int arg_instanceId, NSUrlRequestData arg_request) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.loadRequest', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_request]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future loadHtmlString( + int arg_instanceId, String arg_string, String? arg_baseUrl) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.loadHtmlString', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_string, arg_baseUrl]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future loadFileUrl( + int arg_instanceId, String arg_url, String arg_readAccessUrl) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.loadFileUrl', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_url, arg_readAccessUrl]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future loadFlutterAsset(int arg_instanceId, String arg_key) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.loadFlutterAsset', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_key]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future canGoBack(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.canGoBack', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as bool?)!; + } + } + + Future canGoForward(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.canGoForward', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as bool?)!; + } + } + + Future goBack(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.goBack', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future goForward(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.goForward', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future reload(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.reload', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future getTitle(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.getTitle', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as String?); + } + } + + Future setAllowsBackForwardNavigationGestures( + int arg_instanceId, bool arg_allow) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.setAllowsBackForwardNavigationGestures', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_allow]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setCustomUserAgent( + int arg_instanceId, String? arg_userAgent) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.setCustomUserAgent', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_userAgent]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future evaluateJavaScript( + int arg_instanceId, String arg_javaScriptString) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.evaluateJavaScript', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_javaScriptString]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as Object?); + } + } +} + +class _WKUIDelegateHostApiCodec extends StandardMessageCodec { + const _WKUIDelegateHostApiCodec(); +} + +class WKUIDelegateHostApi { + /// Constructor for [WKUIDelegateHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + WKUIDelegateHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WKUIDelegateHostApiCodec(); + + Future create(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUIDelegateHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _WKHttpCookieStoreHostApiCodec extends StandardMessageCodec { + const _WKHttpCookieStoreHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NSHttpCookieData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is NSHttpCookiePropertyKeyEnumData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NSHttpCookieData.decode(readValue(buffer)!); + + case 129: + return NSHttpCookiePropertyKeyEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class WKHttpCookieStoreHostApi { + /// Constructor for [WKHttpCookieStoreHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + WKHttpCookieStoreHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WKHttpCookieStoreHostApiCodec(); + + Future createFromWebsiteDataStore( + int arg_instanceId, int arg_websiteDataStoreInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKHttpCookieStoreHostApi.createFromWebsiteDataStore', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_websiteDataStoreInstanceId]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setCookie( + int arg_instanceId, NSHttpCookieData arg_cookie) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKHttpCookieStoreHostApi.setCookie', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_cookie]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart index dc933a072b7f..ec7bb5377de0 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart @@ -5,6 +5,165 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import '../common/instance_manager.dart'; +import 'foundation_api_impls.dart'; + +/// The values that can be returned in a change map. +/// +/// Wraps [NSKeyValueObservingOptions](https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions?language=objc). +enum NSKeyValueObservingOptions { + /// Indicates that the change map should provide the new attribute value, if applicable. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions/nskeyvalueobservingoptionnew?language=objc. + newValue, + + /// Indicates that the change map should contain the old attribute value, if applicable. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions/nskeyvalueobservingoptionold?language=objc. + oldValue, + + /// Indicates a notification should be sent to the observer immediately. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions/nskeyvalueobservingoptioninitial?language=objc. + initialValue, + + /// Whether separate notifications should be sent to the observer before and after each change. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions/nskeyvalueobservingoptionprior?language=objc. + priorNotification, +} + +/// The kinds of changes that can be observed. +/// +/// Wraps [NSKeyValueChange](https://developer.apple.com/documentation/foundation/nskeyvaluechange?language=objc). +enum NSKeyValueChange { + /// Indicates that the value of the observed key path was set to a new value. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechange/nskeyvaluechangesetting?language=objc. + setting, + + /// Indicates that an object has been inserted into the to-many relationship that is being observed. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechange/nskeyvaluechangeinsertion?language=objc. + insertion, + + /// Indicates that an object has been removed from the to-many relationship that is being observed. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechange/nskeyvaluechangeremoval?language=objc. + removal, + + /// Indicates that an object has been replaced in the to-many relationship that is being observed. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechange/nskeyvaluechangereplacement?language=objc. + replacement, +} + +/// The keys that can appear in the change map. +/// +/// Wraps [NSKeyValueChangeKey](https://developer.apple.com/documentation/foundation/nskeyvaluechangekey?language=objc). +enum NSKeyValueChangeKey { + /// Indicates changes made in a collection. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechangeindexeskey?language=objc. + indexes, + + /// Indicates what sort of change has occurred. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechangekindkey?language=objc. + kind, + + /// Indicates the new value for the attribute. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechangenewkey?language=objc. + newValue, + + /// Indicates a notification is sent prior to a change. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechangenotificationispriorkey?language=objc. + notificationIsPrior, + + /// Indicates the value of this key is the value before the attribute was changed. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechangeoldkey?language=objc. + oldValue, +} + +/// The supported keys in a cookie attributes dictionary. +/// +/// Wraps [NSHTTPCookiePropertyKey](https://developer.apple.com/documentation/foundation/nshttpcookiepropertykey). +enum NSHttpCookiePropertyKey { + /// A String object containing the comment for the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiecomment. + comment, + + /// A String object containing the comment URL for the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiecommenturl. + commentUrl, + + /// A String object stating whether the cookie should be discarded at the end of the session. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiediscard. + discard, + + /// A String object specifying the expiration date for the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiedomain. + domain, + + /// A String object specifying the expiration date for the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookieexpires. + expires, + + /// A String object containing an integer value stating how long in seconds the cookie should be kept, at most. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiemaximumage. + maximumAge, + + /// A String object containing the name of the cookie (required). + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiename. + name, + + /// A String object containing the URL that set this cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookieoriginurl. + originUrl, + + /// A String object containing the path for the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiepath. + path, + + /// A String object containing comma-separated integer values specifying the ports for the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookieport. + port, + + /// A String indicating the same-site policy for the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiesamesitepolicy. + sameSitePolicy, + + /// A String object indicating that the cookie should be transmitted only over secure channels. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiesecure. + secure, + + /// A String object containing the value of the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookievalue. + value, + + /// A String object that specifies the version of the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookieversion. + version, +} /// A URL load request that is independent of protocol or URL scheme. /// @@ -57,3 +216,68 @@ class NSError { /// A string containing the localized description of the error. final String localizedDescription; } + +/// A representation of an HTTP cookie. +/// +/// Wraps [NSHTTPCookie](https://developer.apple.com/documentation/foundation/nshttpcookie). +@immutable +class NSHttpCookie { + /// Initializes an HTTP cookie object using the provided properties. + const NSHttpCookie.withProperties(this.properties); + + /// Properties of the new cookie object. + final Map properties; +} + +/// The root class of most Objective-C class hierarchies. +class NSObject { + /// Constructs an [NSObject]. + NSObject({BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) + : _api = NSObjectHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ) { + // Ensures FlutterApis for the Foundation library and FunctionFlutterApi are + // set up. + FoundationFlutterApis.instance.ensureSetUp(); + } + + final NSObjectHostApiImpl _api; + + /// Registers the observer object to receive KVO notifications. + Future addObserver( + NSObject observer, { + required String keyPath, + required Set options, + }) { + assert(options.isNotEmpty); + return _api.addObserverForInstances( + this, + observer, + keyPath, + options, + ); + } + + /// Stops the observer object from receiving change notifications for the property. + Future removeObserver(NSObject observer, {required String keyPath}) { + return _api.removeObserverForInstances(this, observer, keyPath); + } + + /// Release the reference to the Objective-C object. + Future dispose() { + return _api.disposeForInstances(this); + } + + /// Informs the observing object when the value at the specified key path has changed. + Future setObserveValue( + void Function( + String keyPath, + NSObject object, + Map change, + )? + observeValue, + ) { + throw UnimplementedError(); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation_api_impls.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation_api_impls.dart new file mode 100644 index 000000000000..b007d1a8312c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation_api_impls.dart @@ -0,0 +1,135 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import '../common/function_flutter_api_impls.dart'; +import '../common/instance_manager.dart'; +import '../common/web_kit.pigeon.dart'; +import 'foundation.dart'; + +Iterable + _toNSKeyValueObservingOptionsEnumData( + Iterable options, +) { + return options.map(( + NSKeyValueObservingOptions option, + ) { + late final NSKeyValueObservingOptionsEnum? value; + switch (option) { + case NSKeyValueObservingOptions.newValue: + value = NSKeyValueObservingOptionsEnum.newValue; + break; + case NSKeyValueObservingOptions.oldValue: + value = NSKeyValueObservingOptionsEnum.oldValue; + break; + case NSKeyValueObservingOptions.initialValue: + value = NSKeyValueObservingOptionsEnum.initialValue; + break; + case NSKeyValueObservingOptions.priorNotification: + value = NSKeyValueObservingOptionsEnum.priorNotification; + break; + } + + return NSKeyValueObservingOptionsEnumData(value: value); + }); +} + +/// Handles initialization of Flutter APIs for the Foundation library. +class FoundationFlutterApis { + /// Constructs a [FoundationFlutterApis]. + @visibleForTesting + FoundationFlutterApis({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _binaryMessenger = binaryMessenger { + functionFlutterApi = + FunctionFlutterApiImpl(instanceManager: instanceManager); + } + + static FoundationFlutterApis _instance = FoundationFlutterApis(); + + /// Sets the global instance containing the Flutter Apis for the Foundation library. + @visibleForTesting + static set instance(FoundationFlutterApis instance) { + _instance = instance; + } + + /// Global instance containing the Flutter Apis for the Foundation library. + static FoundationFlutterApis get instance { + return _instance; + } + + final BinaryMessenger? _binaryMessenger; + bool _hasBeenSetUp = false; + + /// Flutter Api for disposing functions. + /// + /// This FlutterApi is placed here because [FoundationFlutterApis.ensureSetUp] + /// is called inside [NSObject] and [NSObject] is the parent class of all + /// objects. + @visibleForTesting + late final FunctionFlutterApiImpl functionFlutterApi; + + /// Ensures all the Flutter APIs have been set up to receive calls from native code. + void ensureSetUp() { + if (!_hasBeenSetUp) { + FunctionFlutterApi.setup( + functionFlutterApi, + binaryMessenger: _binaryMessenger, + ); + _hasBeenSetUp = true; + } + } +} + +/// Host api implementation for [NSObject]. +class NSObjectHostApiImpl extends NSObjectHostApi { + /// Constructs an [NSObjectHostApiImpl]. + NSObjectHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? InstanceManager.instance, + super(binaryMessenger: binaryMessenger); + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [addObserver] with the ids of the provided object instances. + Future addObserverForInstances( + NSObject instance, + NSObject observer, + String keyPath, + Set options, + ) { + return addObserver( + instanceManager.getInstanceId(instance)!, + instanceManager.getInstanceId(observer)!, + keyPath, + _toNSKeyValueObservingOptionsEnumData(options).toList(), + ); + } + + /// Calls [removeObserver] with the ids of the provided object instances. + Future removeObserverForInstances( + NSObject instance, + NSObject observer, + String keyPath, + ) { + return removeObserver( + instanceManager.getInstanceId(instance)!, + instanceManager.getInstanceId(observer)!, + keyPath, + ); + } + + /// Calls [dispose] with the ids of the provided object instances. + Future disposeForInstances(NSObject instance) async { + final int? instanceId = instanceManager.removeInstance(instance); + if (instanceId != null) { + await dispose(instanceId); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit.dart new file mode 100644 index 000000000000..7c1bdd01724c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit.dart @@ -0,0 +1,86 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/painting.dart' show Color; +import 'package:flutter/services.dart'; + +import '../common/instance_manager.dart'; +import '../foundation/foundation.dart'; +import '../web_kit/web_kit.dart'; +import 'ui_kit_api_impls.dart'; + +/// A view that allows the scrolling and zooming of its contained views. +/// +/// Wraps [UIScrollView](https://developer.apple.com/documentation/uikit/uiscrollview?language=objc). +class UIScrollView extends UIView { + /// Constructs a [UIScrollView] that is owned by [webView]. + UIScrollView.fromWebView( + WKWebView webView, { + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _scrollViewApi = UIScrollViewHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ) { + _scrollViewApi.createFromWebViewForInstances(this, webView); + } + + final UIScrollViewHostApiImpl _scrollViewApi; + + /// Point at which the origin of the content view is offset from the origin of the scroll view. + /// + /// Represents [WKWebView.contentOffset](https://developer.apple.com/documentation/uikit/uiscrollview/1619404-contentoffset?language=objc). + Future> getContentOffset() { + return _scrollViewApi.getContentOffsetForInstances(this); + } + + /// Move the scrolled position of this view. + /// + /// This method is not a part of UIKit and is only a helper method to make + /// scrollBy atomic. + Future scrollBy(Point offset) { + return _scrollViewApi.scrollByForInstances(this, offset); + } + + /// Set point at which the origin of the content view is offset from the origin of the scroll view. + /// + /// The default value is `Point(0.0, 0.0)`. + /// + /// Sets [WKWebView.contentOffset](https://developer.apple.com/documentation/uikit/uiscrollview/1619404-contentoffset?language=objc). + Future setContentOffset(Point offset) { + return _scrollViewApi.setContentOffsetForInstances(this, offset); + } +} + +/// Manages the content for a rectangular area on the screen. +/// +/// Wraps [UIView](https://developer.apple.com/documentation/uikit/uiview?language=objc). +class UIView extends NSObject { + /// Constructs an [NSObject]. + UIView({BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) + : _viewApi = UIViewHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + + final UIViewHostApiImpl _viewApi; + + /// The view’s background color. + /// + /// The default value is null, which results in a transparent background color. + /// + /// Sets [UIView.backgroundColor](https://developer.apple.com/documentation/uikit/uiview/1622591-backgroundcolor?language=objc). + Future setBackgroundColor(Color? color) { + return _viewApi.setBackgroundColorForInstances(this, color); + } + + /// Determines whether the view is opaque. + /// + /// Sets [UIView.opaque](https://developer.apple.com/documentation/uikit/uiview?language=objc). + Future setOpaque(bool opaque) { + return _viewApi.setOpaqueForInstances(this, opaque); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit_api_impls.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit_api_impls.dart new file mode 100644 index 000000000000..b2ca5672f8e2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit_api_impls.dart @@ -0,0 +1,107 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/painting.dart' show Color; +import 'package:flutter/services.dart'; + +import '../common/instance_manager.dart'; +import '../common/web_kit.pigeon.dart'; +import '../web_kit/web_kit.dart'; +import 'ui_kit.dart'; + +/// Host api implementation for [UIScrollView]. +class UIScrollViewHostApiImpl extends UIScrollViewHostApi { + /// Constructs a [UIScrollViewHostApiImpl]. + UIScrollViewHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? InstanceManager.instance, + super(binaryMessenger: binaryMessenger); + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [createFromWebView] with the ids of the provided object instances. + Future createFromWebViewForInstances( + UIScrollView instance, + WKWebView webView, + ) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + await createFromWebView( + instanceId, + instanceManager.getInstanceId(webView)!, + ); + } + } + + /// Calls [getContentOffset] with the ids of the provided object instances. + Future> getContentOffsetForInstances( + UIScrollView instance, + ) async { + final List point = await getContentOffset( + instanceManager.getInstanceId(instance)!, + ); + return Point(point[0]!, point[1]!); + } + + /// Calls [scrollBy] with the ids of the provided object instances. + Future scrollByForInstances( + UIScrollView instance, + Point offset, + ) { + return scrollBy( + instanceManager.getInstanceId(instance)!, + offset.x, + offset.y, + ); + } + + /// Calls [setContentOffset] with the ids of the provided object instances. + Future setContentOffsetForInstances( + UIScrollView instance, + Point offset, + ) async { + return setContentOffset( + instanceManager.getInstanceId(instance)!, + offset.x, + offset.y, + ); + } +} + +/// Host api implementation for [UIView]. +class UIViewHostApiImpl extends UIViewHostApi { + /// Constructs a [UIViewHostApiImpl]. + UIViewHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? InstanceManager.instance, + super(binaryMessenger: binaryMessenger); + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [setBackgroundColor] with the ids of the provided object instances. + Future setBackgroundColorForInstances( + UIView instance, + Color? color, + ) async { + return setBackgroundColor( + instanceManager.getInstanceId(instance)!, + color?.value, + ); + } + + /// Calls [setOpaque] with the ids of the provided object instances. + Future setOpaqueForInstances( + UIView instance, + bool opaque, + ) async { + return setOpaque(instanceManager.getInstanceId(instance)!, opaque); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart index 85881e0e512e..ab63db67bba1 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart @@ -3,8 +3,12 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import '../common/instance_manager.dart'; import '../foundation/foundation.dart'; +import '../ui_kit/ui_kit.dart'; +import 'web_kit_api_impls.dart'; /// Times at which to inject script content into a webpage. /// @@ -46,6 +50,35 @@ enum WKAudiovisualMediaType { all, } +/// Types of data that websites store. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebsitedatarecord/data_store_record_types?language=objc. +enum WKWebsiteDataTypes { + /// Cookies. + cookies, + + /// In-memory caches. + memoryCache, + + /// On-disk caches. + diskCache, + + /// HTML offline web app caches. + offlineWebApplicationCache, + + /// HTML local storage. + localStroage, + + /// HTML session storage. + sessionStorage, + + /// WebSQL databases. + sqlDatabases, + + /// IndexedDB databases. + indexedDBDatabases, +} + /// Indicate whether to allow or cancel navigation to a webpage. /// /// Wraps [WKNavigationActionPolicy](https://developer.apple.com/documentation/webkit/wknavigationactionpolicy?language=objc). @@ -93,6 +126,18 @@ class WKErrorCode { static const int javaScriptResultTypeIsUnsupported = 5; } +/// A record of the data that a particular website stores persistently. +/// +/// Wraps [WKWebsiteDataRecord](https://developer.apple.com/documentation/webkit/wkwebsitedatarecord?language=objc). +@immutable +class WKWebsiteDataRecord { + /// Constructs a [WKWebsiteDataRecord]. + const WKWebsiteDataRecord({required this.displayName}); + + /// Identifying information that you display to users. + final String displayName; +} + /// An object that contains information about an action that causes navigation to occur. /// /// Wraps [WKNavigationAction](https://developer.apple.com/documentation/webkit/wknavigationaction?language=objc). @@ -162,16 +207,162 @@ class WKScriptMessage { final Object? body; } +/// Encapsulates the standard behaviors to apply to websites. +/// +/// Wraps [WKPreferences](https://developer.apple.com/documentation/webkit/wkpreferences?language=objc). +class WKPreferences { + /// Constructs a [WKPreferences] that is owned by [configuration]. + @visibleForTesting + WKPreferences.fromWebViewConfiguration( + WKWebViewConfiguration configuration, { + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _preferencesApi = WKPreferencesHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ) { + _preferencesApi.createFromWebViewConfigurationForInstances( + this, + configuration, + ); + } + + final WKPreferencesHostApiImpl _preferencesApi; + + // TODO(bparrishMines): Deprecated for iOS 14.0+. Add support for alternative. + /// Sets whether JavaScript is enabled. + /// + /// The default value is true. + Future setJavaScriptEnabled(bool enabled) { + return _preferencesApi.setJavaScriptEnabledForInstances(this, enabled); + } +} + +/// Manages cookies, disk and memory caches, and other types of data for a web view. +/// +/// Wraps [WKWebsiteDataStore](https://developer.apple.com/documentation/webkit/wkwebsitedatastore?language=objc). +class WKWebsiteDataStore { + WKWebsiteDataStore._({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _websiteDataStoreApi = WKWebsiteDataStoreHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + + factory WKWebsiteDataStore._defaultDataStore() { + final WKWebsiteDataStore websiteDataStore = WKWebsiteDataStore._(); + websiteDataStore._websiteDataStoreApi.createDefaultDataStoreForInstances( + websiteDataStore, + ); + return websiteDataStore; + } + + /// Constructs a [WKWebsiteDataStore] that is owned by [configuration]. + @visibleForTesting + factory WKWebsiteDataStore.fromWebViewConfiguration( + WKWebViewConfiguration configuration, { + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) { + final WKWebsiteDataStore websiteDataStore = WKWebsiteDataStore._( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + websiteDataStore._websiteDataStoreApi + .createFromWebViewConfigurationForInstances( + websiteDataStore, + configuration, + ); + return websiteDataStore; + } + + /// Default data store that stores data persistently to disk. + static final WKWebsiteDataStore defaultDataStore = + WKWebsiteDataStore._defaultDataStore(); + + final WKWebsiteDataStoreHostApiImpl _websiteDataStoreApi; + + /// Manages the HTTP cookies associated with a particular web view. + late final WKHttpCookieStore httpCookieStore = + WKHttpCookieStore.fromWebsiteDataStore(this); + + /// Removes website data that changed after the specified date. + /// + /// Returns whether any data was removed. + Future removeDataOfTypes( + Set dataTypes, + DateTime since, + ) { + return _websiteDataStoreApi.removeDataOfTypesForInstances( + this, + dataTypes, + secondsModifiedSinceEpoch: since.millisecondsSinceEpoch / 1000, + ); + } +} + +/// An object that manages the HTTP cookies associated with a particular web view. +/// +/// Wraps [WKHTTPCookieStore](https://developer.apple.com/documentation/webkit/wkhttpcookiestore?language=objc). +class WKHttpCookieStore extends NSObject { + WKHttpCookieStore._({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _httpCookieStoreApi = WKHttpCookieStoreHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + + /// Constructs a [WKHttpCookieStore] that is owned by [dataStore]. + @visibleForTesting + factory WKHttpCookieStore.fromWebsiteDataStore( + WKWebsiteDataStore dataStore, { + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) { + final WKHttpCookieStore cookieStore = WKHttpCookieStore._( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + cookieStore._httpCookieStoreApi.createFromWebsiteDataStoreForInstances( + cookieStore, + dataStore, + ); + return cookieStore; + } + + final WKHttpCookieStoreHostApiImpl _httpCookieStoreApi; + + /// Adds a cookie to the cookie store. + Future setCookie(NSHttpCookie cookie) { + return _httpCookieStoreApi.setCookieForInsances(this, cookie); + } +} + /// An interface for receiving messages from JavaScript code running in a webpage. /// /// Wraps [WKScriptMessageHandler](https://developer.apple.com/documentation/webkit/wkscriptmessagehandler?language=objc) class WKScriptMessageHandler { + /// Constructs a [WKScriptMessageHandler]. + WKScriptMessageHandler({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _scriptMessageHandlerApi = WKScriptMessageHandlerHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ) { + _scriptMessageHandlerApi.createForInstances(this); + } + + final WKScriptMessageHandlerHostApiImpl _scriptMessageHandlerApi; + /// Tells the handler that a webpage sent a script message. /// /// Use this method to respond to a message sent from the webpage’s /// JavaScript code. Use the [message] parameter to get the message contents and /// to determine the originating web view. - set didReceiveScriptMessage( + Future setDidReceiveScriptMessage( void Function( WKUserContentController userContentController, WKScriptMessage message, @@ -192,15 +383,23 @@ class WKScriptMessageHandler { /// /// Wraps [WKUserContentController](https://developer.apple.com/documentation/webkit/wkusercontentcontroller?language=objc). class WKUserContentController { - /// Constructs a [WKUserContentController]. - WKUserContentController(); - - // A WKUserContentController that is owned by configuration. - WKUserContentController._fromWebViewConfiguretion( - // TODO(bparrishMines): Remove ignore once constructor is implemented. - // ignore: avoid_unused_constructor_parameters - WKWebViewConfiguration configuration, - ); + /// Constructs a [WKUserContentController] that is owned by [configuration]. + @visibleForTesting + WKUserContentController.fromWebViewConfiguration( + WKWebViewConfiguration configuration, { + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _userContentControllerApi = WKUserContentControllerHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ) { + _userContentControllerApi.createFromWebViewConfigurationForInstances( + this, + configuration, + ); + } + + final WKUserContentControllerHostApiImpl _userContentControllerApi; /// Installs a message handler that you can call from your JavaScript code. /// @@ -218,7 +417,11 @@ class WKUserContentController { String name, ) { assert(name.isNotEmpty); - throw UnimplementedError(); + return _userContentControllerApi.addScriptMessageHandlerForInstances( + this, + handler, + name, + ); } /// Uninstalls the custom message handler with the specified name from your JavaScript code. @@ -231,22 +434,28 @@ class WKUserContentController { /// message handler from the page content world. If you installed the message /// handler in a different content world, this method doesn’t remove it. Future removeScriptMessageHandler(String name) { - throw UnimplementedError(); + return _userContentControllerApi.removeScriptMessageHandlerForInstances( + this, + name, + ); } /// Uninstalls all custom message handlers associated with the user content controller. Future removeAllScriptMessageHandlers() { - throw UnimplementedError(); + return _userContentControllerApi.removeAllScriptMessageHandlersForInstances( + this, + ); } /// Injects the specified script into the webpage’s content. Future addUserScript(WKUserScript userScript) { - throw UnimplementedError(); + return _userContentControllerApi.addUserScriptForInstances( + this, userScript); } /// Removes all user scripts from the web view. Future removeAllUserScripts() { - throw UnimplementedError(); + return _userContentControllerApi.removeAllUserScriptsForInstances(this); } } @@ -255,33 +464,97 @@ class WKUserContentController { /// Wraps [WKWebViewConfiguration](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration?language=objc). class WKWebViewConfiguration { /// Constructs a [WKWebViewConfiguration]. - WKWebViewConfiguration({required this.userContentController}); + factory WKWebViewConfiguration({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) { + final WKWebViewConfiguration configuration = WKWebViewConfiguration._( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + configuration._webViewConfigurationApi.createForInstances(configuration); + return configuration; + } - // A WKWebViewConfiguration that is owned by webView. - // TODO(bparrishMines): Remove ignore once constructor is implemented. - // ignore: avoid_unused_constructor_parameters - WKWebViewConfiguration._fromWebView(WKWebView webView) { - userContentController = - WKUserContentController._fromWebViewConfiguretion(this); + /// A WKWebViewConfiguration that is owned by webView. + @visibleForTesting + factory WKWebViewConfiguration.fromWebView( + WKWebView webView, { + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) { + final WKWebViewConfiguration configuration = WKWebViewConfiguration._( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + configuration._webViewConfigurationApi.createFromWebViewForInstances( + configuration, + webView, + ); + return configuration; } + WKWebViewConfiguration._({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _binaryMessenger = binaryMessenger, + _instanceManager = instanceManager, + _webViewConfigurationApi = WKWebViewConfigurationHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + + final BinaryMessenger? _binaryMessenger; + final InstanceManager? _instanceManager; + + late final WKWebViewConfigurationHostApiImpl _webViewConfigurationApi; + /// Coordinates interactions between your app’s code and the webpage’s scripts and other content. - late final WKUserContentController userContentController; + late final WKUserContentController userContentController = + WKUserContentController.fromWebViewConfiguration( + this, + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, + ); + + /// Manages the preference-related settings for the web view. + late final WKPreferences preferences = + WKPreferences.fromWebViewConfiguration(this); + + /// Used to get and set the site’s cookies and to track the cached data objects. + /// + /// Represents [WKWebViewConfiguration.webSiteDataStore](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/1395661-websitedatastore?language=objc). + late final WKWebsiteDataStore websiteDataStore = + WKWebsiteDataStore.fromWebViewConfiguration( + this, + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, + ); /// Indicates whether HTML5 videos play inline or use the native full-screen controller. - set allowsInlineMediaPlayback(bool allow) { - throw UnimplementedError(); + /// + /// Sets [WKWebViewConfiguration.allowsInlineMediaPlayback](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/1614793-allowsinlinemediaplayback?language=objc). + Future setAllowsInlineMediaPlayback(bool allow) { + return _webViewConfigurationApi.setAllowsInlineMediaPlaybackForInstances( + this, + allow, + ); } /// The media types that require a user gesture to begin playing. /// /// Use [WKAudiovisualMediaType.none] to indicate that no user gestures are /// required to begin playing media. - set mediaTypesRequiringUserActionForPlayback( + /// + /// Sets [WKWebViewConfiguration.mediaTypesRequiringUserActionForPlayback](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/1851524-mediatypesrequiringuseractionfor?language=objc). + Future setMediaTypesRequiringUserActionForPlayback( Set types, ) { - assert(types.isNotEmpty); - throw UnimplementedError(); + return _webViewConfigurationApi + .setMediaTypesRequiringUserActionForPlaybackForInstances( + this, + types, + ); } } @@ -289,13 +562,26 @@ class WKWebViewConfiguration { /// /// Wraps [WKUIDelegate](https://developer.apple.com/documentation/webkit/wkuidelegate?language=objc). class WKUIDelegate { - /// Indicates a new [WebView] was requested to be created with [configuration]. - set onCreateWebView( + /// Constructs a [WKUIDelegate]. + WKUIDelegate({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _uiDelegateApi = WKUIDelegateHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ) { + _uiDelegateApi.createForInstances(this); + } + + final WKUIDelegateHostApiImpl _uiDelegateApi; + + /// Indicates a new [WKWebView] was requested to be created with [configuration]. + Future setOnCreateWebView( void Function( WKWebViewConfiguration configuration, WKNavigationAction navigationAction, )? - onCreateeWebView, + onCreateWebView, ) { throw UnimplementedError(); } @@ -307,27 +593,45 @@ class WKUIDelegate { /// coordinate changes in your web view’s main frame. /// /// Wraps [WKNavigationDelegate](https://developer.apple.com/documentation/webkit/wknavigationdelegate?language=objc). -class WKNavigationDelegate { +class WKNavigationDelegate extends NSObject { + /// Constructs a [WKNavigationDelegate]. + WKNavigationDelegate({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _navigationDelegateApi = WKNavigationDelegateHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ) { + WebKitFlutterApis.instance.ensureSetUp(); + _navigationDelegateApi.createForInstances(this); + } + + final WKNavigationDelegateHostApiImpl _navigationDelegateApi; + /// Called when navigation from the main frame has started. - set didStartProvisionalNavigation( - void Function( - WKWebView webView, - String? url, - )? + Future setDidStartProvisionalNavigation( + void Function(WKWebView webView, String? url)? didStartProvisionalNavigation, ) { throw UnimplementedError(); } /// Called when navigation is complete. - set didFinishNavigation( + Future setDidFinishNavigation( void Function(WKWebView webView, String? url)? didFinishNavigation, ) { - throw UnimplementedError(); + return _navigationDelegateApi.setDidFinishNavigationFromInstance( + this, + didFinishNavigation, + ); } /// Called when permission is needed to navigate to new content. - set decidePolicyForNavigationAction( + Future setDecidePolicyForNavigationAction( Future Function( WKWebView webView, WKNavigationAction navigationAction, @@ -337,14 +641,14 @@ class WKNavigationDelegate { } /// Called when an error occurred during navigation. - set didFailNavigation( + Future setDidFailNavigation( void Function(WKWebView webView, NSError error)? didFailNavigation, ) { throw UnimplementedError(); } /// Called when an error occurred during the early navigation process. - set didFailProvisionalNavigation( + Future setDidFailProvisionalNavigation( void Function(WKWebView webView, NSError error)? didFailProvisionalNavigation, ) { @@ -352,7 +656,7 @@ class WKNavigationDelegate { } /// Called when the web view’s content process was terminated. - set webViewWebContentProcessDidTerminate( + Future setWebViewWebContentProcessDidTerminate( void Function(WKWebView webView)? webViewWebContentProcessDidTerminate, ) { throw UnimplementedError(); @@ -362,7 +666,7 @@ class WKNavigationDelegate { /// Object that displays interactive web content, such as for an in-app browser. /// /// Wraps [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview?language=objc). -class WKWebView { +class WKWebView extends UIView { /// Constructs a [WKWebView]. /// /// [configuration] contains the configuration details for the web view. This @@ -372,12 +676,28 @@ class WKWebView { /// values, see [WKWebViewConfiguration]. If you didn’t create your web view /// using the `configuration` parameter, this value uses a default /// configuration object. - // TODO(bparrishMines): Remove ignore once constructor is implemented. - // ignore: avoid_unused_constructor_parameters - WKWebView([WKWebViewConfiguration? configuration]) { - throw UnimplementedError(); + WKWebView( + WKWebViewConfiguration configuration, { + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _binaryMessenger = binaryMessenger, + _instanceManager = instanceManager, + _webViewApi = WKWebViewHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ) { + _webViewApi.createForInstances(this, configuration); } + final BinaryMessenger? _binaryMessenger; + final InstanceManager? _instanceManager; + + final WKWebViewHostApiImpl _webViewApi; + /// Contains the configuration details for the web view. /// /// Use the object in this property to obtain information about your web @@ -388,21 +708,47 @@ class WKWebView { /// If you didn’t create your web view with a [WKWebViewConfiguration] this /// property contains a default configuration object. late final WKWebViewConfiguration configuration = - WKWebViewConfiguration._fromWebView(this); + WKWebViewConfiguration.fromWebView( + this, + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, + ); + + /// The scrollable view associated with the web view. + late final UIScrollView scrollView = UIScrollView.fromWebView( + this, + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, + ); /// Used to integrate custom user interface elements into web view interactions. - set uiDelegate(WKUIDelegate? delegate) { - throw UnimplementedError(); + /// + /// Sets [WKWebView.UIDelegate](https://developer.apple.com/documentation/webkit/wkwebview/1415009-uidelegate?language=objc). + Future setUIDelegate(WKUIDelegate? delegate) { + return _webViewApi.setUIDelegateForInstances(this, delegate); } /// The object you use to manage navigation behavior for the web view. - set navigationDelegate(WKNavigationDelegate? delegate) { - throw UnimplementedError(); + /// + /// Sets [WKWebView.navigationDelegate](https://developer.apple.com/documentation/webkit/wkwebview/1414971-navigationdelegate?language=objc). + Future setNavigationDelegate(WKNavigationDelegate? delegate) { + return _webViewApi.setNavigationDelegateForInstances(this, delegate); } /// The URL for the current webpage. - Future get url { - throw UnimplementedError(); + /// + /// Represents [WKWebView.URL](https://developer.apple.com/documentation/webkit/wkwebview/1415005-url?language=objc). + Future getUrl() { + return _webViewApi.getUrlForInstances(this); + } + + /// An estimate of what fraction of the current navigation has been loaded. + /// + /// This value ranges from 0.0 to 1.0. + /// + /// Represents [WKWebView.estimatedProgress](https://developer.apple.com/documentation/webkit/wkwebview/1415007-estimatedprogress?language=objc). + Future getEstimatedProgress() { + return _webViewApi.getEstimatedProgressForInstances(this); } /// Loads the web content referenced by the specified URL request object and navigates to it. @@ -410,6 +756,88 @@ class WKWebView { /// Use this method to load a page from a local or network-based URL. For /// example, you might use it to navigate to a network-based webpage. Future loadRequest(NSUrlRequest request) { - throw UnimplementedError(); + return _webViewApi.loadRequestForInstances(this, request); + } + + /// Loads the contents of the specified HTML string and navigates to it. + Future loadHtmlString(String string, {String? baseUrl}) { + return _webViewApi.loadHtmlStringForInstances(this, string, baseUrl); + } + + /// Loads the web content from the specified file and navigates to it. + Future loadFileUrl(String url, {required String readAccessUrl}) { + return _webViewApi.loadFileUrlForInstances(this, url, readAccessUrl); + } + + /// Loads the Flutter asset specified in the pubspec.yaml file. + /// + /// This method is not a part of WebKit and is only a Flutter specific helper + /// method. + Future loadFlutterAsset(String key) { + return _webViewApi.loadFlutterAssetForInstances(this, key); + } + + /// Indicates whether there is a valid back item in the back-forward list. + Future canGoBack() { + return _webViewApi.canGoBackForInstances(this); + } + + /// Indicates whether there is a valid forward item in the back-forward list. + Future canGoForward() { + return _webViewApi.canGoForwardForInstances(this); + } + + /// Navigates to the back item in the back-forward list. + Future goBack() { + return _webViewApi.goBackForInstances(this); + } + + /// Navigates to the forward item in the back-forward list. + Future goForward() { + return _webViewApi.goForwardForInstances(this); + } + + /// Reloads the current webpage. + Future reload() { + return _webViewApi.reloadForInstances(this); + } + + /// The page title. + /// + /// Represents [WKWebView.title](https://developer.apple.com/documentation/webkit/wkwebview/1415015-title?language=objc). + Future getTitle() { + return _webViewApi.getTitleForInstances(this); + } + + /// Indicates whether horizontal swipe gestures trigger page navigation. + /// + /// The default value is false. + /// + /// Sets [WKWebView.allowsBackForwardNavigationGestures](https://developer.apple.com/documentation/webkit/wkwebview/1414995-allowsbackforwardnavigationgestu?language=objc). + Future setAllowsBackForwardNavigationGestures(bool allow) { + return _webViewApi.setAllowsBackForwardNavigationGesturesForInstances( + this, + allow, + ); + } + + /// The custom user agent string. + /// + /// The default value of this property is null. + /// + /// Sets [WKWebView.customUserAgent](https://developer.apple.com/documentation/webkit/wkwebview/1414950-customuseragent?language=objc). + Future setCustomUserAgent(String? userAgent) { + return _webViewApi.setCustomUserAgentForInstances(this, userAgent); + } + + /// Evaluates the specified JavaScript string. + /// + /// Throws a `PlatformException` if an error occurs or return value is not + /// supported. + Future evaluateJavaScript(String javaScriptString) { + return _webViewApi.evaluateJavaScriptForInstances( + this, + javaScriptString, + ); } } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart new file mode 100644 index 000000000000..b970ab26bcc7 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart @@ -0,0 +1,751 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import '../common/instance_manager.dart'; +import '../common/web_kit.pigeon.dart'; +import '../foundation/foundation.dart'; +import 'web_kit.dart'; + +Iterable _toWKWebsiteDataTypesEnumData( + Iterable types) { + return types.map((WKWebsiteDataTypes type) { + late final WKWebsiteDataTypesEnum value; + switch (type) { + case WKWebsiteDataTypes.cookies: + value = WKWebsiteDataTypesEnum.cookies; + break; + case WKWebsiteDataTypes.memoryCache: + value = WKWebsiteDataTypesEnum.memoryCache; + break; + case WKWebsiteDataTypes.diskCache: + value = WKWebsiteDataTypesEnum.diskCache; + break; + case WKWebsiteDataTypes.offlineWebApplicationCache: + value = WKWebsiteDataTypesEnum.offlineWebApplicationCache; + break; + case WKWebsiteDataTypes.localStroage: + value = WKWebsiteDataTypesEnum.localStroage; + break; + case WKWebsiteDataTypes.sessionStorage: + value = WKWebsiteDataTypesEnum.sessionStorage; + break; + case WKWebsiteDataTypes.sqlDatabases: + value = WKWebsiteDataTypesEnum.sqlDatabases; + break; + case WKWebsiteDataTypes.indexedDBDatabases: + value = WKWebsiteDataTypesEnum.indexedDBDatabases; + break; + } + + return WKWebsiteDataTypesEnumData(value: value); + }); +} + +extension _NSHttpCookieConverter on NSHttpCookie { + NSHttpCookieData toNSHttpCookieData() { + return NSHttpCookieData( + properties: properties.map( + (NSHttpCookiePropertyKey key, Object value) { + return MapEntry( + key.toNSHttpCookiePropertyKeyEnumData(), + value.toString(), + ); + }, + ), + ); + } +} + +extension _NSHttpCookiePropertyKeyConverter on NSHttpCookiePropertyKey { + NSHttpCookiePropertyKeyEnumData toNSHttpCookiePropertyKeyEnumData() { + late final NSHttpCookiePropertyKeyEnum value; + switch (this) { + case NSHttpCookiePropertyKey.comment: + value = NSHttpCookiePropertyKeyEnum.comment; + break; + case NSHttpCookiePropertyKey.commentUrl: + value = NSHttpCookiePropertyKeyEnum.commentUrl; + break; + case NSHttpCookiePropertyKey.discard: + value = NSHttpCookiePropertyKeyEnum.discard; + break; + case NSHttpCookiePropertyKey.domain: + value = NSHttpCookiePropertyKeyEnum.domain; + break; + case NSHttpCookiePropertyKey.expires: + value = NSHttpCookiePropertyKeyEnum.expires; + break; + case NSHttpCookiePropertyKey.maximumAge: + value = NSHttpCookiePropertyKeyEnum.maximumAge; + break; + case NSHttpCookiePropertyKey.name: + value = NSHttpCookiePropertyKeyEnum.name; + break; + case NSHttpCookiePropertyKey.originUrl: + value = NSHttpCookiePropertyKeyEnum.originUrl; + break; + case NSHttpCookiePropertyKey.path: + value = NSHttpCookiePropertyKeyEnum.path; + break; + case NSHttpCookiePropertyKey.port: + value = NSHttpCookiePropertyKeyEnum.port; + break; + case NSHttpCookiePropertyKey.sameSitePolicy: + value = NSHttpCookiePropertyKeyEnum.sameSitePolicy; + break; + case NSHttpCookiePropertyKey.secure: + value = NSHttpCookiePropertyKeyEnum.secure; + break; + case NSHttpCookiePropertyKey.value: + value = NSHttpCookiePropertyKeyEnum.value; + break; + case NSHttpCookiePropertyKey.version: + value = NSHttpCookiePropertyKeyEnum.version; + break; + } + + return NSHttpCookiePropertyKeyEnumData(value: value); + } +} + +extension _WKUserScriptInjectionTimeConverter on WKUserScriptInjectionTime { + WKUserScriptInjectionTimeEnumData toWKUserScriptInjectionTimeEnumData() { + late final WKUserScriptInjectionTimeEnum value; + switch (this) { + case WKUserScriptInjectionTime.atDocumentStart: + value = WKUserScriptInjectionTimeEnum.atDocumentStart; + break; + case WKUserScriptInjectionTime.atDocumentEnd: + value = WKUserScriptInjectionTimeEnum.atDocumentEnd; + break; + } + + return WKUserScriptInjectionTimeEnumData(value: value); + } +} + +Iterable _toWKAudiovisualMediaTypeEnumData( + Iterable types, +) { + return types + .map((WKAudiovisualMediaType type) { + late final WKAudiovisualMediaTypeEnum value; + switch (type) { + case WKAudiovisualMediaType.none: + value = WKAudiovisualMediaTypeEnum.none; + break; + case WKAudiovisualMediaType.audio: + value = WKAudiovisualMediaTypeEnum.audio; + break; + case WKAudiovisualMediaType.video: + value = WKAudiovisualMediaTypeEnum.video; + break; + case WKAudiovisualMediaType.all: + value = WKAudiovisualMediaTypeEnum.all; + break; + } + + return WKAudiovisualMediaTypeEnumData(value: value); + }); +} + +extension _WKUserScriptConverter on WKUserScript { + WKUserScriptData toWKUserScriptData() { + return WKUserScriptData( + source: source, + injectionTime: injectionTime.toWKUserScriptInjectionTimeEnumData(), + isMainFrameOnly: isMainFrameOnly, + ); + } +} + +extension _NSUrlRequestConverter on NSUrlRequest { + NSUrlRequestData toNSUrlRequestData() { + return NSUrlRequestData( + url: url, + httpMethod: httpMethod, + httpBody: httpBody, + allHttpHeaderFields: allHttpHeaderFields, + ); + } +} + +/// Handles initialization of Flutter APIs for WebKit. +class WebKitFlutterApis { + /// Constructs a [WebKitFlutterApis]. + @visibleForTesting + WebKitFlutterApis({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _binaryMessenger = binaryMessenger { + navigationDelegateFlutterApi = WKNavigationDelegateFlutterApiImpl( + instanceManager: instanceManager, + ); + } + + static WebKitFlutterApis _instance = WebKitFlutterApis(); + + /// Sets the global instance containing the Flutter Apis for the WebKit library. + @visibleForTesting + static set instance(WebKitFlutterApis instance) { + _instance = instance; + } + + /// Global instance containing the Flutter Apis for the WebKit library. + static WebKitFlutterApis get instance { + return _instance; + } + + final BinaryMessenger? _binaryMessenger; + bool _hasBeenSetUp = false; + + /// Flutter Api for [WKNavigationDelegate]. + @visibleForTesting + late final WKNavigationDelegateFlutterApiImpl navigationDelegateFlutterApi; + + /// Ensures all the Flutter APIs have been set up to receive calls from native code. + void ensureSetUp() { + if (!_hasBeenSetUp) { + WKNavigationDelegateFlutterApi.setup( + navigationDelegateFlutterApi, + binaryMessenger: _binaryMessenger, + ); + _hasBeenSetUp = true; + } + } +} + +/// Host api implementation for [WKWebSiteDataStore]. +class WKWebsiteDataStoreHostApiImpl extends WKWebsiteDataStoreHostApi { + /// Constructs a [WebsiteDataStoreHostApiImpl]. + WKWebsiteDataStoreHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? InstanceManager.instance, + super(binaryMessenger: binaryMessenger); + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [createFromWebViewConfiguration] with the ids of the provided object instances. + Future createFromWebViewConfigurationForInstances( + WKWebsiteDataStore instance, + WKWebViewConfiguration configuration, + ) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + await createFromWebViewConfiguration( + instanceId, + instanceManager.getInstanceId(configuration)!, + ); + } + } + + /// Calls [createDefaultDataStore] with the ids of the provided object instances. + Future createDefaultDataStoreForInstances( + WKWebsiteDataStore instance, + ) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + await createDefaultDataStore(instanceId); + } + } + + /// Calls [removeDataOfTypes] with the ids of the provided object instances. + Future removeDataOfTypesForInstances( + WKWebsiteDataStore instance, + Set dataTypes, { + required double secondsModifiedSinceEpoch, + }) { + return removeDataOfTypes( + instanceManager.getInstanceId(instance)!, + _toWKWebsiteDataTypesEnumData(dataTypes).toList(), + secondsModifiedSinceEpoch, + ); + } +} + +/// Host api implementation for [WKScriptMessageHandler]. +class WKScriptMessageHandlerHostApiImpl extends WKScriptMessageHandlerHostApi { + /// Constructs a [WKScriptMessageHandlerHostApiImpl]. + WKScriptMessageHandlerHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? InstanceManager.instance, + super(binaryMessenger: binaryMessenger); + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [create] with the ids of the provided object instances. + Future createForInstances(WKScriptMessageHandler instance) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + await create(instanceId); + } + } +} + +/// Host api implementation for [WKPreferences]. +class WKPreferencesHostApiImpl extends WKPreferencesHostApi { + /// Constructs a [WKPreferencesHostApiImpl]. + WKPreferencesHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? InstanceManager.instance, + super(binaryMessenger: binaryMessenger); + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [createFromWebViewConfiguration] with the ids of the provided object instances. + Future createFromWebViewConfigurationForInstances( + WKPreferences instance, + WKWebViewConfiguration configuration, + ) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + await createFromWebViewConfiguration( + instanceId, + instanceManager.getInstanceId(configuration)!, + ); + } + } + + /// Calls [setJavaScriptEnabled] with the ids of the provided object instances. + Future setJavaScriptEnabledForInstances( + WKPreferences instance, + bool enabled, + ) { + return setJavaScriptEnabled( + instanceManager.getInstanceId(instance)!, + enabled, + ); + } +} + +/// Host api implementation for [WKHttpCookieStore]. +class WKHttpCookieStoreHostApiImpl extends WKHttpCookieStoreHostApi { + /// Constructs a [WKHttpCookieStoreHostApiImpl]. + WKHttpCookieStoreHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? InstanceManager.instance, + super(binaryMessenger: binaryMessenger); + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [createFromWebsiteDataStore] with the ids of the provided object instances. + Future createFromWebsiteDataStoreForInstances( + WKHttpCookieStore instance, + WKWebsiteDataStore dataStore, + ) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + await createFromWebsiteDataStore( + instanceId, + instanceManager.getInstanceId(dataStore)!, + ); + } + } + + /// Calls [setCookie] with the ids of the provided object instances. + Future setCookieForInsances( + WKHttpCookieStore instance, + NSHttpCookie cookie, + ) { + return setCookie( + instanceManager.getInstanceId(instance)!, + cookie.toNSHttpCookieData(), + ); + } +} + +/// Host api implementation for [WKUserContentController]. +class WKUserContentControllerHostApiImpl + extends WKUserContentControllerHostApi { + /// Constructs a [WKUserContentControllerHostApiImpl]. + WKUserContentControllerHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? InstanceManager.instance, + super(binaryMessenger: binaryMessenger); + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [createFromWebViewConfiguration] with the ids of the provided object instances. + Future createFromWebViewConfigurationForInstances( + WKUserContentController instance, + WKWebViewConfiguration configuration, + ) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + await createFromWebViewConfiguration( + instanceId, + instanceManager.getInstanceId(configuration)!, + ); + } + } + + /// Calls [addScriptMessageHandler] with the ids of the provided object instances. + Future addScriptMessageHandlerForInstances( + WKUserContentController instance, + WKScriptMessageHandler handler, + String name, + ) { + return addScriptMessageHandler( + instanceManager.getInstanceId(instance)!, + instanceManager.getInstanceId(handler)!, + name, + ); + } + + /// Calls [removeScriptMessageHandler] with the ids of the provided object instances. + Future removeScriptMessageHandlerForInstances( + WKUserContentController instance, + String name, + ) { + return removeScriptMessageHandler( + instanceManager.getInstanceId(instance)!, + name, + ); + } + + /// Calls [removeAllScriptMessageHandlers] with the ids of the provided object instances. + Future removeAllScriptMessageHandlersForInstances( + WKUserContentController instance, + ) { + return removeAllScriptMessageHandlers( + instanceManager.getInstanceId(instance)!, + ); + } + + /// Calls [addUserScript] with the ids of the provided object instances. + Future addUserScriptForInstances( + WKUserContentController instance, + WKUserScript userScript, + ) { + return addUserScript( + instanceManager.getInstanceId(instance)!, + userScript.toWKUserScriptData(), + ); + } + + /// Calls [removeAllUserScripts] with the ids of the provided object instances. + Future removeAllUserScriptsForInstances( + WKUserContentController instance, + ) { + return removeAllUserScripts(instanceManager.getInstanceId(instance)!); + } +} + +/// Host api implementation for [WKWebViewConfiguration]. +class WKWebViewConfigurationHostApiImpl extends WKWebViewConfigurationHostApi { + /// Constructs a [WKWebViewConfigurationHostApiImpl]. + WKWebViewConfigurationHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? InstanceManager.instance, + super(binaryMessenger: binaryMessenger); + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [create] with the ids of the provided object instances. + Future createForInstances(WKWebViewConfiguration instance) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + await create(instanceId); + } + } + + /// Calls [createFromWebView] with the ids of the provided object instances. + Future createFromWebViewForInstances( + WKWebViewConfiguration instance, + WKWebView webView, + ) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + await createFromWebView( + instanceId, + instanceManager.getInstanceId(webView)!, + ); + } + } + + /// Calls [setAllowsInlineMediaPlayback] with the ids of the provided object instances. + Future setAllowsInlineMediaPlaybackForInstances( + WKWebViewConfiguration instance, + bool allow, + ) { + return setAllowsInlineMediaPlayback( + instanceManager.getInstanceId(instance)!, + allow, + ); + } + + /// Calls [setMediaTypesRequiringUserActionForPlayback] with the ids of the provided object instances. + Future setMediaTypesRequiringUserActionForPlaybackForInstances( + WKWebViewConfiguration instance, + Set types, + ) { + return setMediaTypesRequiringUserActionForPlayback( + instanceManager.getInstanceId(instance)!, + _toWKAudiovisualMediaTypeEnumData(types).toList(), + ); + } +} + +/// Host api implementation for [WKUIDelegate]. +class WKUIDelegateHostApiImpl extends WKUIDelegateHostApi { + /// Constructs a [WKUIDelegateHostApiImpl]. + WKUIDelegateHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? InstanceManager.instance, + super(binaryMessenger: binaryMessenger); + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [create] with the ids of the provided object instances. + Future createForInstances(WKUIDelegate instance) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + await create(instanceId); + } + } +} + +/// Host api implementation for [WKNavigationDelegate]. +class WKNavigationDelegateHostApiImpl extends WKNavigationDelegateHostApi { + /// Constructs a [WKNavigationDelegateHostApiImpl]. + WKNavigationDelegateHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? InstanceManager.instance, + super(binaryMessenger: binaryMessenger); + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [create] with the ids of the provided object instances. + Future createForInstances(WKNavigationDelegate instance) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + await create(instanceId); + } + } + + /// Calls [setDidFinishNavigation] with the ids of the provided object instances. + Future setDidFinishNavigationFromInstance( + WKNavigationDelegate instance, + void Function(WKWebView, String?)? didFinishNavigation, + ) { + int? functionInstanceId; + if (didFinishNavigation != null) { + functionInstanceId = instanceManager.getInstanceId(didFinishNavigation) ?? + instanceManager.tryAddInstance(didFinishNavigation)!; + } + return setDidFinishNavigation( + instanceManager.getInstanceId(instance)!, + functionInstanceId, + ); + } +} + +/// Flutter api implementation for [WKNavigationDelegate]. +class WKNavigationDelegateFlutterApiImpl + extends WKNavigationDelegateFlutterApi { + /// Constructs a [WKNavigationDelegateFlutterApiImpl]. + WKNavigationDelegateFlutterApiImpl({InstanceManager? instanceManager}) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with native language objects. + late final InstanceManager instanceManager; + + @override + void didFinishNavigation( + int functionInstanceId, + int webViewInstanceId, + String? url, + ) { + final void Function( + WKWebView webView, + String? url, + ) function = instanceManager.getInstance(functionInstanceId)!; + function(instanceManager.getInstance(webViewInstanceId)!, url); + } +} + +/// Host api implementation for [WKWebView]. +class WKWebViewHostApiImpl extends WKWebViewHostApi { + /// Constructs a [WKWebViewHostApiImpl]. + WKWebViewHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? InstanceManager.instance, + super(binaryMessenger: binaryMessenger); + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [create] with the ids of the provided object instances. + Future createForInstances( + WKWebView instance, + WKWebViewConfiguration configuration, + ) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + await create( + instanceId, + instanceManager.getInstanceId(configuration)!, + ); + } + } + + /// Calls [loadRequest] with the ids of the provided object instances. + Future loadRequestForInstances( + WKWebView webView, NSUrlRequest request) { + return loadRequest( + instanceManager.getInstanceId(webView)!, + request.toNSUrlRequestData(), + ); + } + + /// Calls [loadHtmlString] with the ids of the provided object instances. + Future loadHtmlStringForInstances( + WKWebView instance, + String string, + String? baseUrl, + ) { + return loadHtmlString( + instanceManager.getInstanceId(instance)!, + string, + baseUrl, + ); + } + + /// Calls [loadFileUrl] with the ids of the provided object instances. + Future loadFileUrlForInstances( + WKWebView instance, + String url, + String readAccessUrl, + ) { + return loadFileUrl( + instanceManager.getInstanceId(instance)!, + url, + readAccessUrl, + ); + } + + /// Calls [loadFlutterAsset] with the ids of the provided object instances. + Future loadFlutterAssetForInstances(WKWebView instance, String key) { + return loadFlutterAsset( + instanceManager.getInstanceId(instance)!, + key, + ); + } + + /// Calls [canGoBack] with the ids of the provided object instances. + Future canGoBackForInstances(WKWebView instance) { + return canGoBack(instanceManager.getInstanceId(instance)!); + } + + /// Calls [canGoForward] with the ids of the provided object instances. + Future canGoForwardForInstances(WKWebView instance) { + return canGoForward(instanceManager.getInstanceId(instance)!); + } + + /// Calls [goBack] with the ids of the provided object instances. + Future goBackForInstances(WKWebView instance) { + return goBack(instanceManager.getInstanceId(instance)!); + } + + /// Calls [goForward] with the ids of the provided object instances. + Future goForwardForInstances(WKWebView instance) { + return goForward(instanceManager.getInstanceId(instance)!); + } + + /// Calls [reload] with the ids of the provided object instances. + Future reloadForInstances(WKWebView instance) { + return reload(instanceManager.getInstanceId(instance)!); + } + + /// Calls [getUrl] with the ids of the provided object instances. + Future getUrlForInstances(WKWebView instance) { + return getUrl(instanceManager.getInstanceId(instance)!); + } + + /// Calls [getTitle] with the ids of the provided object instances. + Future getTitleForInstances(WKWebView instance) { + return getTitle(instanceManager.getInstanceId(instance)!); + } + + /// Calls [getEstimatedProgress] with the ids of the provided object instances. + Future getEstimatedProgressForInstances(WKWebView instance) { + return getEstimatedProgress(instanceManager.getInstanceId(instance)!); + } + + /// Calls [setAllowsBackForwardNavigationGestures] with the ids of the provided object instances. + Future setAllowsBackForwardNavigationGesturesForInstances( + WKWebView instance, + bool allow, + ) { + return setAllowsBackForwardNavigationGestures( + instanceManager.getInstanceId(instance)!, + allow, + ); + } + + /// Calls [setCustomUserAgent] with the ids of the provided object instances. + Future setCustomUserAgentForInstances( + WKWebView instance, + String? userAgent, + ) { + return setCustomUserAgent( + instanceManager.getInstanceId(instance)!, + userAgent, + ); + } + + /// Calls [evaluateJavaScript] with the ids of the provided object instances. + Future evaluateJavaScriptForInstances( + WKWebView instance, + String javaScriptString, + ) { + return evaluateJavaScript( + instanceManager.getInstanceId(instance)!, + javaScriptString, + ); + } + + /// Calls [setNavigationDelegate] with the ids of the provided object instances. + Future setNavigationDelegateForInstances( + WKWebView instance, + WKNavigationDelegate? delegate, + ) { + return setNavigationDelegate( + instanceManager.getInstanceId(instance)!, + delegate != null ? instanceManager.getInstanceId(delegate)! : null, + ); + } + + /// Calls [setUIDelegate] with the ids of the provided object instances. + Future setUIDelegateForInstances( + WKWebView instance, + WKUIDelegate? delegate, + ) { + return setUIDelegate( + instanceManager.getInstanceId(instance)!, + delegate != null ? instanceManager.getInstanceId(delegate)! : null, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_cookie_manager.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_cookie_manager.dart new file mode 100644 index 000000000000..c73111d73fd0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_cookie_manager.dart @@ -0,0 +1,54 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; + +/// Handles all cookie operations for the WebView platform. +class WebKitCookieManager extends WebViewCookieManagerPlatform { + /// Constructs a [WebKitCookieManager]. + WebKitCookieManager({WKWebsiteDataStore? websiteDataStore}) + : websiteDataStore = + websiteDataStore ?? WKWebsiteDataStore.defaultDataStore; + + /// Manages stored data for [WKWebView]s. + final WKWebsiteDataStore websiteDataStore; + + @override + Future clearCookies() async { + return websiteDataStore.removeDataOfTypes( + {WKWebsiteDataTypes.cookies}, + DateTime.fromMillisecondsSinceEpoch(0), + ); + } + + @override + Future setCookie(WebViewCookie cookie) { + if (!_isValidPath(cookie.path)) { + throw ArgumentError( + 'The path property for the provided cookie was not given a legal value.'); + } + + return websiteDataStore.httpCookieStore.setCookie( + NSHttpCookie.withProperties( + { + NSHttpCookiePropertyKey.name: cookie.name, + NSHttpCookiePropertyKey.value: cookie.value, + NSHttpCookiePropertyKey.domain: cookie.domain, + NSHttpCookiePropertyKey.path: cookie.path, + }, + ), + ); + } + + bool _isValidPath(String path) { + // Permitted ranges based on RFC6265bis: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 + return !path.codeUnits.any( + (int char) { + return (char < 0x20 || char > 0x3A) && (char < 0x3C || char > 0x7E); + }, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart index 89a258e75767..012cd221599b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart @@ -3,9 +3,12 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:math'; import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:path/path.dart' as path; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'foundation/foundation.dart'; @@ -83,23 +86,12 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { }) : super(callbacksHandler) { _setCreationParams( creationParams, - configuration: configuration ?? - WKWebViewConfiguration( - userContentController: WKUserContentController(), - ), + configuration: configuration ?? WKWebViewConfiguration(), ); - - webView.uiDelegate = uiDelegate; - uiDelegate.onCreateWebView = ( - WKWebViewConfiguration configuration, - WKNavigationAction navigationAction, - ) { - if (!navigationAction.targetFrame.isMainFrame) { - webView.loadRequest(navigationAction.request); - } - }; } + bool _zoomEnabled = true; + final Map _scriptMessageHandlers = {}; @@ -125,19 +117,19 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { @visibleForTesting late final WKNavigationDelegate navigationDelegate = webViewProxy.createNavigationDelegate() - ..didStartProvisionalNavigation = (WKWebView webView, String? url) { + ..setDidStartProvisionalNavigation((WKWebView webView, String? url) { callbacksHandler.onPageStarted(url ?? ''); - } - ..didFinishNavigation = (WKWebView webView, String? url) { + }) + ..setDidFinishNavigation((WKWebView webView, String? url) { callbacksHandler.onPageFinished(url ?? ''); - } - ..didFailNavigation = (WKWebView webView, NSError error) { + }) + ..setDidFailNavigation((WKWebView webView, NSError error) { callbacksHandler.onWebResourceError(_toWebResourceError(error)); - } - ..didFailProvisionalNavigation = (WKWebView webView, NSError error) { + }) + ..setDidFailProvisionalNavigation((WKWebView webView, NSError error) { callbacksHandler.onWebResourceError(_toWebResourceError(error)); - } - ..webViewWebContentProcessDidTerminate = (WKWebView webView) { + }) + ..setWebViewWebContentProcessDidTerminate((WKWebView webView) { callbacksHandler.onWebResourceError(WebResourceError( errorCode: WKErrorCode.webContentProcessTerminated, // Value from https://developer.apple.com/documentation/webkit/wkerrordomain?language=objc. @@ -145,7 +137,7 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { description: '', errorType: WebResourceErrorType.webContentProcessTerminated, )); - }; + }); Future _setCreationParams( CreationParams params, { @@ -159,13 +151,37 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { webView = webViewProxy.createWebView(configuration); + webView.setUIDelegate(uiDelegate); + uiDelegate.setOnCreateWebView(( + WKWebViewConfiguration configuration, + WKNavigationAction navigationAction, + ) { + if (!navigationAction.targetFrame.isMainFrame) { + webView.loadRequest(navigationAction.request); + } + }); + await addJavascriptChannels(params.javascriptChannelNames); - webView.navigationDelegate = navigationDelegate; + webView.setNavigationDelegate(navigationDelegate); + + if (params.userAgent != null) { + webView.setCustomUserAgent(params.userAgent!); + } if (params.webSettings != null) { updateSettings(params.webSettings!); } + + if (params.backgroundColor != null) { + webView.setOpaque(false); + webView.setBackgroundColor(Colors.transparent); + webView.scrollView.setBackgroundColor(params.backgroundColor!); + } + + if (params.initialUrl != null) { + await loadUrl(params.initialUrl!, null); + } } void _setWebViewConfiguration( @@ -174,7 +190,7 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { required AutoMediaPlaybackPolicy autoMediaPlaybackPolicy, }) { if (allowsInlineMediaPlayback != null) { - configuration.allowsInlineMediaPlayback = allowsInlineMediaPlayback; + configuration.setAllowsInlineMediaPlayback(allowsInlineMediaPlayback); } late final bool requiresUserAction; @@ -187,18 +203,173 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { break; } - configuration.mediaTypesRequiringUserActionForPlayback = - { + configuration + .setMediaTypesRequiringUserActionForPlayback({ if (requiresUserAction) WKAudiovisualMediaType.all, if (!requiresUserAction) WKAudiovisualMediaType.none, - }; + }); } @override - Future updateSettings(WebSettings setting) async { - if (setting.hasNavigationDelegate != null) { - _setHasNavigationDelegate(setting.hasNavigationDelegate!); + Future loadHtmlString(String html, {String? baseUrl}) { + return webView.loadHtmlString(html, baseUrl: baseUrl); + } + + @override + Future loadFile(String absoluteFilePath) async { + await webView.loadFileUrl( + absoluteFilePath, + readAccessUrl: path.dirname(absoluteFilePath), + ); + } + + @override + Future clearCache() { + return webView.configuration.websiteDataStore.removeDataOfTypes( + { + WKWebsiteDataTypes.memoryCache, + WKWebsiteDataTypes.diskCache, + WKWebsiteDataTypes.offlineWebApplicationCache, + WKWebsiteDataTypes.localStroage, + }, + DateTime.fromMillisecondsSinceEpoch(0), + ); + } + + @override + Future loadFlutterAsset(String key) async { + assert(key.isNotEmpty); + return webView.loadFlutterAsset(key); + } + + @override + Future loadUrl(String url, Map? headers) async { + final NSUrlRequest request = NSUrlRequest( + url: url, + allHttpHeaderFields: headers ?? {}, + ); + return webView.loadRequest(request); + } + + @override + Future loadRequest(WebViewRequest request) async { + if (!request.uri.hasScheme) { + throw ArgumentError('WebViewRequest#uri is required to have a scheme.'); + } + + final NSUrlRequest urlRequest = NSUrlRequest( + url: request.uri.toString(), + allHttpHeaderFields: request.headers, + httpMethod: describeEnum(request.method), + httpBody: request.body, + ); + + return webView.loadRequest(urlRequest); + } + + @override + Future canGoBack() => webView.canGoBack(); + + @override + Future canGoForward() => webView.canGoForward(); + + @override + Future goBack() => webView.goBack(); + + @override + Future goForward() => webView.goForward(); + + @override + Future reload() => webView.reload(); + + @override + Future evaluateJavascript(String javascript) async { + final Object? result = await webView.evaluateJavaScript(javascript); + // The legacy implementation of webview_flutter_wkwebview would convert + // objects to strings before returning them to Dart. This method attempts + // to converts Dart objects to Strings the way it is done in Objective-C + // to avoid breaking users expecting the same String format. + return _asObjectiveCString(result); + } + + @override + Future runJavascript(String javascript) async { + try { + await webView.evaluateJavaScript(javascript); + } on PlatformException catch (exception) { + // WebKit will throw an error when the type of the evaluated value is + // unsupported. This also goes for `null` and `undefined` on iOS 14+. For + // example, when running a void function. For ease of use, this specific + // error is ignored when no return value is expected. + // TODO(bparrishMines): Ensure the platform code includes the NSError in + // the FlutterError.details. + if (exception.details is! NSError || + exception.details.code != + WKErrorCode.javaScriptResultTypeIsUnsupported) { + rethrow; + } + } + } + + @override + Future runJavascriptReturningResult(String javascript) async { + final Object? result = await webView.evaluateJavaScript(javascript); + if (result == null) { + throw ArgumentError( + 'Result of JavaScript execution returned a `null` value. ' + 'Use `runJavascript` when expecting a null return value.', + ); } + return result.toString(); + } + + @override + Future getTitle() => webView.getTitle(); + + @override + Future scrollTo(int x, int y) async { + webView.scrollView.setContentOffset(Point( + x.toDouble(), + y.toDouble(), + )); + } + + @override + Future scrollBy(int x, int y) async { + await webView.scrollView.scrollBy(Point( + x.toDouble(), + y.toDouble(), + )); + } + + @override + Future getScrollX() async { + final Point offset = await webView.scrollView.getContentOffset(); + return offset.x.toInt(); + } + + @override + Future getScrollY() async { + final Point offset = await webView.scrollView.getContentOffset(); + return offset.y.toInt(); + } + + @override + Future updateSettings(WebSettings setting) async { + await Future.wait(>[ + _setUserAgent(setting.userAgent), + if (setting.hasNavigationDelegate != null) + _setHasNavigationDelegate(setting.hasNavigationDelegate!), + if (setting.hasProgressTracking != null) + _setHasProgressTracking(setting.hasProgressTracking!), + if (setting.javascriptMode != null) + _setJavaScriptMode(setting.javascriptMode!), + if (setting.zoomEnabled != null) _setZoomEnabled(setting.zoomEnabled!), + if (setting.gestureNavigationEnabled != null) + webView.setAllowsBackForwardNavigationGestures( + setting.gestureNavigationEnabled!, + ), + ]); } @override @@ -212,7 +383,7 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { (String channelName) { final WKScriptMessageHandler handler = webViewProxy.createScriptMessageHandler() - ..didReceiveScriptMessage = ( + ..setDidReceiveScriptMessage(( WKUserContentController userContentController, WKScriptMessage message, ) { @@ -220,7 +391,7 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { message.name, message.body!.toString(), ); - }; + }); _scriptMessageHandlers[channelName] = handler; final String wrapperSource = @@ -250,24 +421,12 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { return; } - // WKWebView does not support removing a single user script, so this removes - // all user scripts and all message handlers and re-registers channels that - // shouldn't be removed. Note that this workaround could interfere with - // exposing support for custom scripts from applications. - webView.configuration.userContentController.removeAllUserScripts(); - webView.configuration.userContentController - .removeAllScriptMessageHandlers(); - - javascriptChannelNames.forEach(_scriptMessageHandlers.remove); - final Set remainingNames = _scriptMessageHandlers.keys.toSet(); - _scriptMessageHandlers.clear(); - - await addJavascriptChannels(remainingNames); + await _resetUserScripts(removedJavaScriptChannels: javascriptChannelNames); } - void _setHasNavigationDelegate(bool hasNavigationDelegate) { + Future _setHasNavigationDelegate(bool hasNavigationDelegate) { if (hasNavigationDelegate) { - navigationDelegate.decidePolicyForNavigationAction = + return navigationDelegate.setDecidePolicyForNavigationAction( (WKWebView webView, WKNavigationAction action) async { final bool allow = await callbacksHandler.onNavigationRequest( url: action.request.url, @@ -277,10 +436,99 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { return allow ? WKNavigationActionPolicy.allow : WKNavigationActionPolicy.cancel; - }; + }); + } else { + return navigationDelegate.setDecidePolicyForNavigationAction(null); + } + } + + Future _setHasProgressTracking(bool hasProgressTracking) { + if (hasProgressTracking) { + webView.setObserveValue(( + String keyPath, + NSObject object, + Map change, + ) { + final double progress = change[NSKeyValueChangeKey.newValue]! as double; + callbacksHandler.onProgress((progress * 100).round()); + }); + return webView.addObserver( + webView, + keyPath: 'estimatedProgress', + options: { + NSKeyValueObservingOptions.newValue, + }, + ); } else { - navigationDelegate.decidePolicyForNavigationAction = null; + webView.setObserveValue(null); + return webView.removeObserver(webView, keyPath: 'estimatedProgress'); + } + } + + Future _setJavaScriptMode(JavascriptMode mode) { + switch (mode) { + case JavascriptMode.disabled: + return webView.configuration.preferences.setJavaScriptEnabled(false); + case JavascriptMode.unrestricted: + return webView.configuration.preferences.setJavaScriptEnabled(true); + } + } + + Future _setUserAgent(WebSetting userAgent) async { + if (userAgent.isPresent) { + await webView.setCustomUserAgent(userAgent.value); + } + } + + Future _setZoomEnabled(bool zoomEnabled) async { + if (_zoomEnabled == zoomEnabled) { + return; } + + _zoomEnabled = zoomEnabled; + if (!zoomEnabled) { + return _disableZoom(); + } + + return _resetUserScripts(); + } + + Future _disableZoom() { + const WKUserScript userScript = WKUserScript( + "var meta = document.createElement('meta');" + "meta.name = 'viewport';" + "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0," + "user-scalable=no';" + "var head = document.getElementsByTagName('head')[0];head.appendChild(meta);", + WKUserScriptInjectionTime.atDocumentEnd, + isMainFrameOnly: true, + ); + return webView.configuration.userContentController + .addUserScript(userScript); + } + + // WkWebView does not support removing a single user script, so all user + // scripts and all message handlers are removed instead. And the JavaScript + // channels that shouldn't be removed are re-registered. Note that this + // workaround could interfere with exposing support for custom scripts from + // applications. + Future _resetUserScripts({ + Set removedJavaScriptChannels = const {}, + }) async { + webView.configuration.userContentController.removeAllUserScripts(); + webView.configuration.userContentController + .removeAllScriptMessageHandlers(); + + removedJavaScriptChannels.forEach(_scriptMessageHandlers.remove); + final Set remainingNames = _scriptMessageHandlers.keys.toSet(); + _scriptMessageHandlers.clear(); + + await Future.wait(>[ + addJavascriptChannels(remainingNames), + // Zoom is disabled with a WKUserScript, so this adds it back if it was + // removed above. + if (!_zoomEnabled) _disableZoom(), + ]); } static WebResourceError _toWebResourceError(NSError error) { @@ -311,6 +559,35 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { errorType: errorType, ); } + + String _asObjectiveCString(Object? value, {bool inContainer = false}) { + if (value == null) { + // An NSNull inside an NSArray or NSDictionary is represented as a String + // differently than a nil. + if (inContainer) { + return '""'; + } + return '(null)'; + } else if (value is List) { + final List stringValues = []; + for (final Object? listValue in value) { + stringValues.add(_asObjectiveCString(listValue, inContainer: true)); + } + return '(${stringValues.join(',')})'; + } else if (value is Map) { + final List stringValues = []; + for (final MapEntry entry in value.entries) { + stringValues.add( + '${_asObjectiveCString(entry.key, inContainer: true)} ' + '= ' + '${_asObjectiveCString(entry.value, inContainer: true)}', + ); + } + return '{${stringValues.join(';')}}'; + } + + return value.toString(); + } } /// Handles constructing objects and calling static methods. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart new file mode 100644 index 000000000000..509fa8e1f7dd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart @@ -0,0 +1,502 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/common/web_kit.pigeon.dart', + dartTestOut: 'test/src/common/test_web_kit.pigeon.dart', + dartOptions: DartOptions(copyrightHeader: [ + 'Copyright 2013 The Flutter Authors. All rights reserved.', + 'Use of this source code is governed by a BSD-style license that can be', + 'found in the LICENSE file.', + ]), + objcHeaderOut: 'ios/Classes/FWFGeneratedWebKitApis.h', + objcSourceOut: 'ios/Classes/FWFGeneratedWebKitApis.m', + objcOptions: ObjcOptions( + header: 'ios/Classes/FWFGeneratedWebKitApis.h', + prefix: 'FWF', + copyrightHeader: [ + 'Copyright 2013 The Flutter Authors. All rights reserved.', + 'Use of this source code is governed by a BSD-style license that can be', + 'found in the LICENSE file.', + ], + ), + ), +) + +/// Mirror of NSKeyValueObservingOptions. +/// +/// See https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions?language=objc. +enum NSKeyValueObservingOptionsEnum { + newValue, + oldValue, + initialValue, + priorNotification, +} + +class NSKeyValueObservingOptionsEnumData { + // TODO(bparrishMines): Generated code fails when enums are marked as nonnull. + // Change to nonnull once this is fixed: https://github.com/flutter/flutter/issues/100594 + late NSKeyValueObservingOptionsEnum? value; +} + +/// Mirror of NSKeyValueChange. +/// +/// See https://developer.apple.com/documentation/foundation/nskeyvaluechange?language=objc. +enum NSKeyValueChangeEnum { + setting, + insertion, + removal, + replacement, +} + +class NSKeyValueChangeEnumData { + late NSKeyValueChangeEnum? value; +} + +/// Mirror of NSKeyValueChangeKey. +/// +/// See https://developer.apple.com/documentation/foundation/nskeyvaluechangekey?language=objc. +enum NSKeyValueChangeKeyEnum { + indexes, + kind, + newValue, + notificationIsPrior, + oldValue, +} + +class NSKeyValueChangeKeyEnumData { + late NSKeyValueChangeKeyEnum? value; +} + +/// Mirror of WKUserScriptInjectionTime. +/// +/// See https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime?language=objc. +enum WKUserScriptInjectionTimeEnum { + atDocumentStart, + atDocumentEnd, +} + +class WKUserScriptInjectionTimeEnumData { + late WKUserScriptInjectionTimeEnum? value; +} + +/// Mirror of WKAudiovisualMediaTypes. +/// +/// See [WKAudiovisualMediaTypes](https://developer.apple.com/documentation/webkit/wkaudiovisualmediatypes?language=objc). +enum WKAudiovisualMediaTypeEnum { + none, + audio, + video, + all, +} + +class WKAudiovisualMediaTypeEnumData { + late WKAudiovisualMediaTypeEnum? value; +} + +/// Mirror of WKWebsiteDataTypes. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebsitedatarecord/data_store_record_types?language=objc. +enum WKWebsiteDataTypesEnum { + cookies, + memoryCache, + diskCache, + offlineWebApplicationCache, + localStroage, + sessionStorage, + sqlDatabases, + indexedDBDatabases, +} + +class WKWebsiteDataTypesEnumData { + late WKWebsiteDataTypesEnum? value; +} + +/// Mirror of WKNavigationActionPolicy. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationactionpolicy?language=objc. +enum WKNavigationActionPolicyEnum { + allow, + cancel, +} + +class WKNavigationActionPolicyEnumData { + late WKNavigationActionPolicyEnum? value; +} + +/// Mirror of NSHTTPCookiePropertyKey. +/// +/// See https://developer.apple.com/documentation/foundation/nshttpcookiepropertykey. +enum NSHttpCookiePropertyKeyEnum { + comment, + commentUrl, + discard, + domain, + expires, + maximumAge, + name, + originUrl, + path, + port, + sameSitePolicy, + secure, + value, + version, +} + +class NSHttpCookiePropertyKeyEnumData { + late NSHttpCookiePropertyKeyEnum? value; +} + +/// Mirror of NSURLRequest. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlrequest?language=objc. +class NSUrlRequestData { + late String url; + late String? httpMethod; + late Uint8List? httpBody; + late Map allHttpHeaderFields; +} + +/// Mirror of WKUserScript. +/// +/// See https://developer.apple.com/documentation/webkit/wkuserscript?language=objc. +class WKUserScriptData { + late String source; + late WKUserScriptInjectionTimeEnumData? injectionTime; + late bool isMainFrameOnly; +} + +/// Mirror of WKNavigationAction. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationaction. +class WKNavigationActionData { + late NSUrlRequestData request; + late WKFrameInfoData targetFrame; +} + +/// Mirror of WKFrameInfo. +/// +/// See https://developer.apple.com/documentation/webkit/wkframeinfo?language=objc. +class WKFrameInfoData { + late bool isMainFrame; +} + +/// Mirror of NSError. +/// +/// See https://developer.apple.com/documentation/foundation/nserror?language=objc. +class NSErrorData { + late int code; + late String domain; + late String localiziedDescription; +} + +/// Mirror of WKScriptMessage. +/// +/// See https://developer.apple.com/documentation/webkit/wkscriptmessage?language=objc. +class WKScriptMessageData { + late String name; + late Object? body; +} + +/// Mirror of NSHttpCookieData. +/// +/// See https://developer.apple.com/documentation/foundation/nshttpcookie?language=objc. +class NSHttpCookieData { + late Map properties; +} + +/// Mirror of WKWebsiteDataStore. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebsitedatastore?language=objc. +@HostApi(dartHostTestHandler: 'TestWKWebsiteDataStoreHostApi') +abstract class WKWebsiteDataStoreHostApi { + @ObjCSelector( + 'createDataStoreFromConfigurationWithIdentifier:configurationIdentifier:', + ) + void createFromWebViewConfiguration( + int instanceId, + int configurationInstanceId, + ); + + @ObjCSelector('createDefaultDataStoreWithIdentifier:') + void createDefaultDataStore(int instanceId); + + @ObjCSelector( + 'removeDataFromDataStoreWithIdentifier:ofTypes:secondsModifiedSinceEpoch:', + ) + @async + bool removeDataOfTypes( + int instanceId, + List dataTypes, + double secondsModifiedSinceEpoch, + ); +} + +/// Mirror of UIView. +/// +/// See https://developer.apple.com/documentation/uikit/uiview?language=objc. +@HostApi(dartHostTestHandler: 'TestUIViewHostApi') +abstract class UIViewHostApi { + @ObjCSelector('contentOffsetForViewWithIdentifier:') + List getContentOffset(int instanceId); + + @ObjCSelector('setBackgroundColorForViewWithIdentifier:toValue:') + void setBackgroundColor(int instanceId, int? value); + + @ObjCSelector('setOpaqueForViewWithIdentifier:isOpaque:') + void setOpaque(int instanceId, bool opaque); +} + +/// Mirror of UIScrollView. +/// +/// See https://developer.apple.com/documentation/uikit/uiscrollview?language=objc. +@HostApi(dartHostTestHandler: 'TestUIScrollViewHostApi') +abstract class UIScrollViewHostApi { + @ObjCSelector('createFromWebViewWithIdentifier:webViewIdentifier:') + void createFromWebView(int instanceId, int webViewInstanceId); + + @ObjCSelector('contentOffsetForScrollViewWithIdentifier:') + List getContentOffset(int instanceId); + + @ObjCSelector('scrollByForScrollViewWithIdentifier:toX:y:') + void scrollBy(int instanceId, double x, double y); + + @ObjCSelector('setContentOffsetForScrollViewWithIdentifier:toX:y:') + void setContentOffset(int instanceId, double x, double y); +} + +/// Mirror of WKWebViewConfiguration. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebviewconfiguration?language=objc. +@HostApi(dartHostTestHandler: 'TestWKWebViewConfigurationHostApi') +abstract class WKWebViewConfigurationHostApi { + @ObjCSelector('createWithIdentifier:') + void create(int instanceId); + + @ObjCSelector('createFromWebViewWithIdentifier:webViewIdentifier:') + void createFromWebView(int instanceId, int webViewInstanceId); + + @ObjCSelector( + 'setAllowsInlineMediaPlaybackForConfigurationWithIdentifier:isAlowed:', + ) + void setAllowsInlineMediaPlayback(int instanceId, bool allow); + + @ObjCSelector( + 'setMediaTypesRequiresUserActionForConfigurationWithIdentifier:forTypes:', + ) + void setMediaTypesRequiringUserActionForPlayback( + int instanceId, + List types, + ); +} + +/// Mirror of WKUserContentController. +/// +/// See https://developer.apple.com/documentation/webkit/wkusercontentcontroller?language=objc. +@HostApi(dartHostTestHandler: 'TestWKUserContentControllerHostApi') +abstract class WKUserContentControllerHostApi { + @ObjCSelector( + 'createFromWebViewConfigurationWithIdentifier:configurationIdentifier:', + ) + void createFromWebViewConfiguration( + int instanceId, + int configurationInstanceId, + ); + + @ObjCSelector( + 'addScriptMessageHandlerForControllerWithIdentifier:handlerIdentifier:ofName:', + ) + void addScriptMessageHandler( + int instanceId, + int handlerInstanceid, + String name, + ); + + @ObjCSelector('removeScriptMessageHandlerForControllerWithIdentifier:name:') + void removeScriptMessageHandler(int instanceId, String name); + + @ObjCSelector('removeAllScriptMessageHandlersForControllerWithIdentifier:') + void removeAllScriptMessageHandlers(int instanceId); + + @ObjCSelector('addUserScriptForControllerWithIdentifier:userScript:') + void addUserScript(int instanceId, WKUserScriptData userScript); + + @ObjCSelector('removeAllUserScriptsForControllerWithIdentifier:') + void removeAllUserScripts(int instanceId); +} + +/// Mirror of WKUserPreferences. +/// +/// See https://developer.apple.com/documentation/webkit/wkpreferences?language=objc. +@HostApi(dartHostTestHandler: 'TestWKPreferencesHostApi') +abstract class WKPreferencesHostApi { + @ObjCSelector('createFromWebViewConfiguration:configurationIdentifier:') + void createFromWebViewConfiguration( + int instanceId, + int configurationInstanceId, + ); + + @ObjCSelector('setJavaScriptEnabledForPreferencesWithIdentifier:isEnabled:') + void setJavaScriptEnabled(int instanceId, bool enabled); +} + +/// Mirror of WKScriptMessageHandler. +/// +/// See https://developer.apple.com/documentation/webkit/wkscriptmessagehandler?language=objc. +@HostApi(dartHostTestHandler: 'TestWKScriptMessageHandlerHostApi') +abstract class WKScriptMessageHandlerHostApi { + @ObjCSelector('createWithIdentifier:') + void create(int instanceId); +} + +/// Mirror of WKNavigationDelegate. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationdelegate?language=objc. +@HostApi(dartHostTestHandler: 'TestWKNavigationDelegateHostApi') +abstract class WKNavigationDelegateHostApi { + @ObjCSelector('createWithIdentifier:') + void create(int instanceId); + + @ObjCSelector( + 'setDidFinishNavigationForDelegateWithIdentifier:functionIdentifier:', + ) + void setDidFinishNavigation(int instanceId, int? functionInstanceId); +} + +/// Mirror of WKNavigationDelegate. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationdelegate?language=objc. +@FlutterApi() +abstract class WKNavigationDelegateFlutterApi { + @ObjCSelector( + 'didFinishNavigationForDelegateWithIdentifier:webViewIdentifier:URL:', + ) + void didFinishNavigation( + int functionInstanceId, + int webViewInstanceId, + String? url, + ); +} + +/// Mirror of NSObject. +/// +/// See https://developer.apple.com/documentation/objectivec/nsobject. +@HostApi(dartHostTestHandler: 'TestNSObjectHostApi') +abstract class NSObjectHostApi { + @ObjCSelector('disposeObjectWithIdentifier:') + void dispose(int instanceId); + + @ObjCSelector( + 'addObserverForObjectWithIdentifier:observerIdentifier:keyPath:options:', + ) + void addObserver( + int instanceId, + int observerInstanceId, + String keyPath, + List options, + ); + + @ObjCSelector( + 'removeObserverForObjectWithIdentifier:observerIdentifier:keyPath:', + ) + void removeObserver(int instanceId, int observerInstanceId, String keyPath); +} + +/// Disposes references to functions. +@FlutterApi() +abstract class FunctionFlutterApi { + @ObjCSelector('disposeFunctionWithIdentifier:') + void dispose(int instanceId); +} + +/// Mirror of WKWebView. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebview?language=objc. +@HostApi(dartHostTestHandler: 'TestWKWebViewHostApi') +abstract class WKWebViewHostApi { + @ObjCSelector('createWithIdentifier:configurationIdentifier:') + void create(int instanceId, int configurationInstanceId); + + @ObjCSelector('setUIDelegateForWebViewWithIdentifier:delegateIdentifier:') + void setUIDelegate(int instanceId, int? uiDelegateInstanceId); + + @ObjCSelector( + 'setNavigationDelegateForWebViewWithIdentifier:delegateIdentifier:', + ) + void setNavigationDelegate(int instanceId, int? navigationDelegateInstanceId); + + @ObjCSelector('URLForWebViewWithIdentifier:') + String? getUrl(int instanceId); + + @ObjCSelector('estimatedProgressForWebViewWithIdentifier:') + double getEstimatedProgress(int instanceId); + + @ObjCSelector('loadRequestForWebViewWithIdentifier:request:') + void loadRequest(int instanceId, NSUrlRequestData request); + + @ObjCSelector('loadHTMLForWebViewWithIdentifier:HTMLString:baseURL:') + void loadHtmlString(int instanceId, String string, String? baseUrl); + + @ObjCSelector('loadFileForWebViewWithIdentifier:fileURL:readAccessURL:') + void loadFileUrl(int instanceId, String url, String readAccessUrl); + + @ObjCSelector('loadAssetForWebViewWithIdentifier:assetKey:') + void loadFlutterAsset(int instanceId, String key); + + @ObjCSelector('canGoBackForWebViewWithIdentifier:') + bool canGoBack(int instanceId); + + @ObjCSelector('canGoForwardForWebViewWithIdentifier:') + bool canGoForward(int instanceId); + + @ObjCSelector('goBackForWebViewWithIdentifier:') + void goBack(int instanceId); + + @ObjCSelector('goForwardForWebViewWithIdentifier:') + void goForward(int instanceId); + + @ObjCSelector('reloadWebViewWithIdentifier:') + void reload(int instanceId); + + @ObjCSelector('titleForWebViewWithIdentifier:') + String? getTitle(int instanceId); + + @ObjCSelector('setAllowsBackForwardForWebViewWithIdentifier:isAllowed:') + void setAllowsBackForwardNavigationGestures(int instanceId, bool allow); + + @ObjCSelector('setUserAgentForWebViewWithIdentifier:userAgent:') + void setCustomUserAgent(int instanceId, String? userAgent); + + @ObjCSelector('evaluateJavaScriptForWebViewWithIdentifier:javaScriptString:') + @async + Object? evaluateJavaScript(int instanceId, String javaScriptString); +} + +/// Mirror of WKUIDelegate. +/// +/// See https://developer.apple.com/documentation/webkit/wkuidelegate?language=objc. +@HostApi(dartHostTestHandler: 'TestWKUIDelegateHostApi') +abstract class WKUIDelegateHostApi { + @ObjCSelector('createWithIdentifier:') + void create(int instanceId); +} + +/// Mirror of WKHttpCookieStore. +/// +/// See https://developer.apple.com/documentation/webkit/wkhttpcookiestore?language=objc. +@HostApi(dartHostTestHandler: 'TestWKHttpCookieStoreHostApi') +abstract class WKHttpCookieStoreHostApi { + @ObjCSelector('createFromWebsiteDataStoreWithIdentifier:dataStoreIdentifier:') + void createFromWebsiteDataStore( + int instanceId, + int websiteDataStoreInstanceId, + ); + + @ObjCSelector('setCookieForStoreWithIdentifier:cookie:') + void setCookie(int instanceId, NSHttpCookieData cookie); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 91760df02c66..f4e72b8f14eb 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_wkwebview description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.7.1 +version: 2.7.3 environment: sdk: ">=2.14.0 <3.0.0" @@ -18,6 +18,7 @@ flutter: dependencies: flutter: sdk: flutter + path: ^1.8.0 webview_flutter_platform_interface: ^1.8.0 dev_dependencies: @@ -28,3 +29,4 @@ dev_dependencies: sdk: flutter mockito: ^5.1.0 pedantic: ^1.10.0 + pigeon: ^3.0.3 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/function_flutter_api_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/function_flutter_api_test.dart new file mode 100644 index 000000000000..63e59386ceaf --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/function_flutter_api_test.dart @@ -0,0 +1,33 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; +import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart'; +import 'package:webview_flutter_wkwebview/src/foundation/foundation_api_impls.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$FunctionFlutterApi', () { + late InstanceManager instanceManager; + + setUp(() { + instanceManager = InstanceManager(); + }); + + test('dispose', () { + final Function function = () {}; + final int functionInstanceId = instanceManager.tryAddInstance(function)!; + + FoundationFlutterApis.instance = FoundationFlutterApis( + instanceManager: instanceManager, + )..ensureSetUp(); + + FoundationFlutterApis.instance.functionFlutterApi + .dispose(functionInstanceId); + expect(instanceManager.getInstanceId(function), isNull); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/instance_manager_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/instance_manager_test.dart new file mode 100644 index 000000000000..10956c0a4aba --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/instance_manager_test.dart @@ -0,0 +1,35 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; + +void main() { + group('InstanceManager', () { + late InstanceManager testInstanceManager; + + setUp(() { + testInstanceManager = InstanceManager(); + }); + + test('tryAddInstance', () { + final Object object = Object(); + + expect(testInstanceManager.tryAddInstance(object), 0); + expect(testInstanceManager.getInstanceId(object), 0); + expect(testInstanceManager.getInstance(0), object); + expect(testInstanceManager.tryAddInstance(object), null); + }); + + test('removeInstance', () { + final Object object = Object(); + testInstanceManager.tryAddInstance(object); + + expect(testInstanceManager.removeInstance(object), 0); + expect(testInstanceManager.getInstanceId(object), null); + expect(testInstanceManager.getInstance(0), null); + expect(testInstanceManager.removeInstance(object), null); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.pigeon.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.pigeon.dart new file mode 100644 index 000000000000..042ddedbd769 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.pigeon.dart @@ -0,0 +1,1471 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.0.3), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis +// ignore_for_file: avoid_relative_lib_imports +// @dart = 2.12 +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; +import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart'; + +class _TestWKWebsiteDataStoreHostApiCodec extends StandardMessageCodec { + const _TestWKWebsiteDataStoreHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is WKWebsiteDataTypesEnumData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WKWebsiteDataTypesEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestWKWebsiteDataStoreHostApi { + static const MessageCodec codec = + _TestWKWebsiteDataStoreHostApiCodec(); + + void createFromWebViewConfiguration( + int instanceId, int configurationInstanceId); + void createDefaultDataStore(int instanceId); + Future removeDataOfTypes( + int instanceId, + List dataTypes, + double secondsModifiedSinceEpoch); + static void setup(TestWKWebsiteDataStoreHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createFromWebViewConfiguration', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createFromWebViewConfiguration was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createFromWebViewConfiguration was null, expected non-null int.'); + final int? arg_configurationInstanceId = (args[1] as int?); + assert(arg_configurationInstanceId != null, + 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createFromWebViewConfiguration was null, expected non-null int.'); + api.createFromWebViewConfiguration( + arg_instanceId!, arg_configurationInstanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createDefaultDataStore', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createDefaultDataStore was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createDefaultDataStore was null, expected non-null int.'); + api.createDefaultDataStore(arg_instanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebsiteDataStoreHostApi.removeDataOfTypes', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.removeDataOfTypes was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.removeDataOfTypes was null, expected non-null int.'); + final List? arg_dataTypes = + (args[1] as List?)?.cast(); + assert(arg_dataTypes != null, + 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.removeDataOfTypes was null, expected non-null List.'); + final double? arg_secondsModifiedSinceEpoch = (args[2] as double?); + assert(arg_secondsModifiedSinceEpoch != null, + 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.removeDataOfTypes was null, expected non-null double.'); + final bool output = await api.removeDataOfTypes( + arg_instanceId!, arg_dataTypes!, arg_secondsModifiedSinceEpoch!); + return {'result': output}; + }); + } + } + } +} + +class _TestUIViewHostApiCodec extends StandardMessageCodec { + const _TestUIViewHostApiCodec(); +} + +abstract class TestUIViewHostApi { + static const MessageCodec codec = _TestUIViewHostApiCodec(); + + List getContentOffset(int instanceId); + void setBackgroundColor(int instanceId, int? value); + void setOpaque(int instanceId, bool opaque); + static void setup(TestUIViewHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIViewHostApi.getContentOffset', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UIViewHostApi.getContentOffset was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.UIViewHostApi.getContentOffset was null, expected non-null int.'); + final List output = api.getContentOffset(arg_instanceId!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIViewHostApi.setBackgroundColor', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UIViewHostApi.setBackgroundColor was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.UIViewHostApi.setBackgroundColor was null, expected non-null int.'); + final int? arg_value = (args[1] as int?); + api.setBackgroundColor(arg_instanceId!, arg_value); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIViewHostApi.setOpaque', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UIViewHostApi.setOpaque was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.UIViewHostApi.setOpaque was null, expected non-null int.'); + final bool? arg_opaque = (args[1] as bool?); + assert(arg_opaque != null, + 'Argument for dev.flutter.pigeon.UIViewHostApi.setOpaque was null, expected non-null bool.'); + api.setOpaque(arg_instanceId!, arg_opaque!); + return {}; + }); + } + } + } +} + +class _TestUIScrollViewHostApiCodec extends StandardMessageCodec { + const _TestUIScrollViewHostApiCodec(); +} + +abstract class TestUIScrollViewHostApi { + static const MessageCodec codec = _TestUIScrollViewHostApiCodec(); + + void createFromWebView(int instanceId, int webViewInstanceId); + List getContentOffset(int instanceId); + void scrollBy(int instanceId, double x, double y); + void setContentOffset(int instanceId, double x, double y); + static void setup(TestUIScrollViewHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIScrollViewHostApi.createFromWebView', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.createFromWebView was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.createFromWebView was null, expected non-null int.'); + final int? arg_webViewInstanceId = (args[1] as int?); + assert(arg_webViewInstanceId != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.createFromWebView was null, expected non-null int.'); + api.createFromWebView(arg_instanceId!, arg_webViewInstanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIScrollViewHostApi.getContentOffset', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.getContentOffset was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.getContentOffset was null, expected non-null int.'); + final List output = api.getContentOffset(arg_instanceId!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIScrollViewHostApi.scrollBy', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.scrollBy was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.scrollBy was null, expected non-null int.'); + final double? arg_x = (args[1] as double?); + assert(arg_x != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.scrollBy was null, expected non-null double.'); + final double? arg_y = (args[2] as double?); + assert(arg_y != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.scrollBy was null, expected non-null double.'); + api.scrollBy(arg_instanceId!, arg_x!, arg_y!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIScrollViewHostApi.setContentOffset', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.setContentOffset was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.setContentOffset was null, expected non-null int.'); + final double? arg_x = (args[1] as double?); + assert(arg_x != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.setContentOffset was null, expected non-null double.'); + final double? arg_y = (args[2] as double?); + assert(arg_y != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.setContentOffset was null, expected non-null double.'); + api.setContentOffset(arg_instanceId!, arg_x!, arg_y!); + return {}; + }); + } + } + } +} + +class _TestWKWebViewConfigurationHostApiCodec extends StandardMessageCodec { + const _TestWKWebViewConfigurationHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is WKAudiovisualMediaTypeEnumData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WKAudiovisualMediaTypeEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestWKWebViewConfigurationHostApi { + static const MessageCodec codec = + _TestWKWebViewConfigurationHostApiCodec(); + + void create(int instanceId); + void createFromWebView(int instanceId, int webViewInstanceId); + void setAllowsInlineMediaPlayback(int instanceId, bool allow); + void setMediaTypesRequiringUserActionForPlayback( + int instanceId, List types); + static void setup(TestWKWebViewConfigurationHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.create was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.create was null, expected non-null int.'); + api.create(arg_instanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.createFromWebView', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.createFromWebView was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.createFromWebView was null, expected non-null int.'); + final int? arg_webViewInstanceId = (args[1] as int?); + assert(arg_webViewInstanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.createFromWebView was null, expected non-null int.'); + api.createFromWebView(arg_instanceId!, arg_webViewInstanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.setAllowsInlineMediaPlayback', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setAllowsInlineMediaPlayback was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setAllowsInlineMediaPlayback was null, expected non-null int.'); + final bool? arg_allow = (args[1] as bool?); + assert(arg_allow != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setAllowsInlineMediaPlayback was null, expected non-null bool.'); + api.setAllowsInlineMediaPlayback(arg_instanceId!, arg_allow!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.setMediaTypesRequiringUserActionForPlayback', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setMediaTypesRequiringUserActionForPlayback was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setMediaTypesRequiringUserActionForPlayback was null, expected non-null int.'); + final List? arg_types = + (args[1] as List?) + ?.cast(); + assert(arg_types != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setMediaTypesRequiringUserActionForPlayback was null, expected non-null List.'); + api.setMediaTypesRequiringUserActionForPlayback( + arg_instanceId!, arg_types!); + return {}; + }); + } + } + } +} + +class _TestWKUserContentControllerHostApiCodec extends StandardMessageCodec { + const _TestWKUserContentControllerHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is WKUserScriptData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptInjectionTimeEnumData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WKUserScriptData.decode(readValue(buffer)!); + + case 129: + return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestWKUserContentControllerHostApi { + static const MessageCodec codec = + _TestWKUserContentControllerHostApiCodec(); + + void createFromWebViewConfiguration( + int instanceId, int configurationInstanceId); + void addScriptMessageHandler( + int instanceId, int handlerInstanceid, String name); + void removeScriptMessageHandler(int instanceId, String name); + void removeAllScriptMessageHandlers(int instanceId); + void addUserScript(int instanceId, WKUserScriptData userScript); + void removeAllUserScripts(int instanceId); + static void setup(TestWKUserContentControllerHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.createFromWebViewConfiguration', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.createFromWebViewConfiguration was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.createFromWebViewConfiguration was null, expected non-null int.'); + final int? arg_configurationInstanceId = (args[1] as int?); + assert(arg_configurationInstanceId != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.createFromWebViewConfiguration was null, expected non-null int.'); + api.createFromWebViewConfiguration( + arg_instanceId!, arg_configurationInstanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.addScriptMessageHandler', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.addScriptMessageHandler was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.addScriptMessageHandler was null, expected non-null int.'); + final int? arg_handlerInstanceid = (args[1] as int?); + assert(arg_handlerInstanceid != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.addScriptMessageHandler was null, expected non-null int.'); + final String? arg_name = (args[2] as String?); + assert(arg_name != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.addScriptMessageHandler was null, expected non-null String.'); + api.addScriptMessageHandler( + arg_instanceId!, arg_handlerInstanceid!, arg_name!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.removeScriptMessageHandler', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.removeScriptMessageHandler was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.removeScriptMessageHandler was null, expected non-null int.'); + final String? arg_name = (args[1] as String?); + assert(arg_name != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.removeScriptMessageHandler was null, expected non-null String.'); + api.removeScriptMessageHandler(arg_instanceId!, arg_name!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllScriptMessageHandlers', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllScriptMessageHandlers was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllScriptMessageHandlers was null, expected non-null int.'); + api.removeAllScriptMessageHandlers(arg_instanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.addUserScript', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.addUserScript was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.addUserScript was null, expected non-null int.'); + final WKUserScriptData? arg_userScript = + (args[1] as WKUserScriptData?); + assert(arg_userScript != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.addUserScript was null, expected non-null WKUserScriptData.'); + api.addUserScript(arg_instanceId!, arg_userScript!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllUserScripts', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllUserScripts was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllUserScripts was null, expected non-null int.'); + api.removeAllUserScripts(arg_instanceId!); + return {}; + }); + } + } + } +} + +class _TestWKPreferencesHostApiCodec extends StandardMessageCodec { + const _TestWKPreferencesHostApiCodec(); +} + +abstract class TestWKPreferencesHostApi { + static const MessageCodec codec = _TestWKPreferencesHostApiCodec(); + + void createFromWebViewConfiguration( + int instanceId, int configurationInstanceId); + void setJavaScriptEnabled(int instanceId, bool enabled); + static void setup(TestWKPreferencesHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKPreferencesHostApi.createFromWebViewConfiguration', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKPreferencesHostApi.createFromWebViewConfiguration was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKPreferencesHostApi.createFromWebViewConfiguration was null, expected non-null int.'); + final int? arg_configurationInstanceId = (args[1] as int?); + assert(arg_configurationInstanceId != null, + 'Argument for dev.flutter.pigeon.WKPreferencesHostApi.createFromWebViewConfiguration was null, expected non-null int.'); + api.createFromWebViewConfiguration( + arg_instanceId!, arg_configurationInstanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKPreferencesHostApi.setJavaScriptEnabled', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKPreferencesHostApi.setJavaScriptEnabled was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKPreferencesHostApi.setJavaScriptEnabled was null, expected non-null int.'); + final bool? arg_enabled = (args[1] as bool?); + assert(arg_enabled != null, + 'Argument for dev.flutter.pigeon.WKPreferencesHostApi.setJavaScriptEnabled was null, expected non-null bool.'); + api.setJavaScriptEnabled(arg_instanceId!, arg_enabled!); + return {}; + }); + } + } + } +} + +class _TestWKScriptMessageHandlerHostApiCodec extends StandardMessageCodec { + const _TestWKScriptMessageHandlerHostApiCodec(); +} + +abstract class TestWKScriptMessageHandlerHostApi { + static const MessageCodec codec = + _TestWKScriptMessageHandlerHostApiCodec(); + + void create(int instanceId); + static void setup(TestWKScriptMessageHandlerHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKScriptMessageHandlerHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKScriptMessageHandlerHostApi.create was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKScriptMessageHandlerHostApi.create was null, expected non-null int.'); + api.create(arg_instanceId!); + return {}; + }); + } + } + } +} + +class _TestWKNavigationDelegateHostApiCodec extends StandardMessageCodec { + const _TestWKNavigationDelegateHostApiCodec(); +} + +abstract class TestWKNavigationDelegateHostApi { + static const MessageCodec codec = + _TestWKNavigationDelegateHostApiCodec(); + + void create(int instanceId); + void setDidFinishNavigation(int instanceId, int? functionInstanceId); + static void setup(TestWKNavigationDelegateHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKNavigationDelegateHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateHostApi.create was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateHostApi.create was null, expected non-null int.'); + api.create(arg_instanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKNavigationDelegateHostApi.setDidFinishNavigation', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateHostApi.setDidFinishNavigation was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateHostApi.setDidFinishNavigation was null, expected non-null int.'); + final int? arg_functionInstanceId = (args[1] as int?); + api.setDidFinishNavigation(arg_instanceId!, arg_functionInstanceId); + return {}; + }); + } + } + } +} + +class _TestNSObjectHostApiCodec extends StandardMessageCodec { + const _TestNSObjectHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NSKeyValueObservingOptionsEnumData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NSKeyValueObservingOptionsEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestNSObjectHostApi { + static const MessageCodec codec = _TestNSObjectHostApiCodec(); + + void dispose(int instanceId); + void addObserver(int instanceId, int observerInstanceId, String keyPath, + List options); + void removeObserver(int instanceId, int observerInstanceId, String keyPath); + static void setup(TestNSObjectHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NSObjectHostApi.dispose', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.dispose was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.dispose was null, expected non-null int.'); + api.dispose(arg_instanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NSObjectHostApi.addObserver', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.addObserver was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.addObserver was null, expected non-null int.'); + final int? arg_observerInstanceId = (args[1] as int?); + assert(arg_observerInstanceId != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.addObserver was null, expected non-null int.'); + final String? arg_keyPath = (args[2] as String?); + assert(arg_keyPath != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.addObserver was null, expected non-null String.'); + final List? arg_options = + (args[3] as List?) + ?.cast(); + assert(arg_options != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.addObserver was null, expected non-null List.'); + api.addObserver(arg_instanceId!, arg_observerInstanceId!, + arg_keyPath!, arg_options!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NSObjectHostApi.removeObserver', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.removeObserver was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.removeObserver was null, expected non-null int.'); + final int? arg_observerInstanceId = (args[1] as int?); + assert(arg_observerInstanceId != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.removeObserver was null, expected non-null int.'); + final String? arg_keyPath = (args[2] as String?); + assert(arg_keyPath != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.removeObserver was null, expected non-null String.'); + api.removeObserver( + arg_instanceId!, arg_observerInstanceId!, arg_keyPath!); + return {}; + }); + } + } + } +} + +class _TestWKWebViewHostApiCodec extends StandardMessageCodec { + const _TestWKWebViewHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NSHttpCookieData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is NSHttpCookiePropertyKeyEnumData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is NSKeyValueObservingOptionsEnumData) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is NSUrlRequestData) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is WKAudiovisualMediaTypeEnumData) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptData) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptInjectionTimeEnumData) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is WKWebsiteDataTypesEnumData) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NSHttpCookieData.decode(readValue(buffer)!); + + case 129: + return NSHttpCookiePropertyKeyEnumData.decode(readValue(buffer)!); + + case 130: + return NSKeyValueObservingOptionsEnumData.decode(readValue(buffer)!); + + case 131: + return NSUrlRequestData.decode(readValue(buffer)!); + + case 132: + return WKAudiovisualMediaTypeEnumData.decode(readValue(buffer)!); + + case 133: + return WKUserScriptData.decode(readValue(buffer)!); + + case 134: + return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + + case 135: + return WKWebsiteDataTypesEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestWKWebViewHostApi { + static const MessageCodec codec = _TestWKWebViewHostApiCodec(); + + void create(int instanceId, int configurationInstanceId); + void setUIDelegate(int instanceId, int? uiDelegateInstanceId); + void setNavigationDelegate(int instanceId, int? navigationDelegateInstanceId); + String? getUrl(int instanceId); + double getEstimatedProgress(int instanceId); + void loadRequest(int instanceId, NSUrlRequestData request); + void loadHtmlString(int instanceId, String string, String? baseUrl); + void loadFileUrl(int instanceId, String url, String readAccessUrl); + void loadFlutterAsset(int instanceId, String key); + bool canGoBack(int instanceId); + bool canGoForward(int instanceId); + void goBack(int instanceId); + void goForward(int instanceId); + void reload(int instanceId); + String? getTitle(int instanceId); + void setAllowsBackForwardNavigationGestures(int instanceId, bool allow); + void setCustomUserAgent(int instanceId, String? userAgent); + Future evaluateJavaScript(int instanceId, String javaScriptString); + static void setup(TestWKWebViewHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.create was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.create was null, expected non-null int.'); + final int? arg_configurationInstanceId = (args[1] as int?); + assert(arg_configurationInstanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.create was null, expected non-null int.'); + api.create(arg_instanceId!, arg_configurationInstanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.setUIDelegate', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setUIDelegate was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setUIDelegate was null, expected non-null int.'); + final int? arg_uiDelegateInstanceId = (args[1] as int?); + api.setUIDelegate(arg_instanceId!, arg_uiDelegateInstanceId); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.setNavigationDelegate', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setNavigationDelegate was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setNavigationDelegate was null, expected non-null int.'); + final int? arg_navigationDelegateInstanceId = (args[1] as int?); + api.setNavigationDelegate( + arg_instanceId!, arg_navigationDelegateInstanceId); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.getUrl', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.getUrl was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.getUrl was null, expected non-null int.'); + final String? output = api.getUrl(arg_instanceId!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.getEstimatedProgress', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.getEstimatedProgress was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.getEstimatedProgress was null, expected non-null int.'); + final double output = api.getEstimatedProgress(arg_instanceId!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.loadRequest', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadRequest was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadRequest was null, expected non-null int.'); + final NSUrlRequestData? arg_request = (args[1] as NSUrlRequestData?); + assert(arg_request != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadRequest was null, expected non-null NSUrlRequestData.'); + api.loadRequest(arg_instanceId!, arg_request!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.loadHtmlString', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadHtmlString was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadHtmlString was null, expected non-null int.'); + final String? arg_string = (args[1] as String?); + assert(arg_string != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadHtmlString was null, expected non-null String.'); + final String? arg_baseUrl = (args[2] as String?); + api.loadHtmlString(arg_instanceId!, arg_string!, arg_baseUrl); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.loadFileUrl', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadFileUrl was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadFileUrl was null, expected non-null int.'); + final String? arg_url = (args[1] as String?); + assert(arg_url != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadFileUrl was null, expected non-null String.'); + final String? arg_readAccessUrl = (args[2] as String?); + assert(arg_readAccessUrl != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadFileUrl was null, expected non-null String.'); + api.loadFileUrl(arg_instanceId!, arg_url!, arg_readAccessUrl!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.loadFlutterAsset', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadFlutterAsset was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadFlutterAsset was null, expected non-null int.'); + final String? arg_key = (args[1] as String?); + assert(arg_key != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadFlutterAsset was null, expected non-null String.'); + api.loadFlutterAsset(arg_instanceId!, arg_key!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.canGoBack', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.canGoBack was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.canGoBack was null, expected non-null int.'); + final bool output = api.canGoBack(arg_instanceId!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.canGoForward', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.canGoForward was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.canGoForward was null, expected non-null int.'); + final bool output = api.canGoForward(arg_instanceId!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.goBack', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.goBack was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.goBack was null, expected non-null int.'); + api.goBack(arg_instanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.goForward', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.goForward was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.goForward was null, expected non-null int.'); + api.goForward(arg_instanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.reload', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.reload was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.reload was null, expected non-null int.'); + api.reload(arg_instanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.getTitle', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.getTitle was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.getTitle was null, expected non-null int.'); + final String? output = api.getTitle(arg_instanceId!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.setAllowsBackForwardNavigationGestures', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setAllowsBackForwardNavigationGestures was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setAllowsBackForwardNavigationGestures was null, expected non-null int.'); + final bool? arg_allow = (args[1] as bool?); + assert(arg_allow != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setAllowsBackForwardNavigationGestures was null, expected non-null bool.'); + api.setAllowsBackForwardNavigationGestures( + arg_instanceId!, arg_allow!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.setCustomUserAgent', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setCustomUserAgent was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setCustomUserAgent was null, expected non-null int.'); + final String? arg_userAgent = (args[1] as String?); + api.setCustomUserAgent(arg_instanceId!, arg_userAgent); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.evaluateJavaScript', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.evaluateJavaScript was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.evaluateJavaScript was null, expected non-null int.'); + final String? arg_javaScriptString = (args[1] as String?); + assert(arg_javaScriptString != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.evaluateJavaScript was null, expected non-null String.'); + final Object? output = await api.evaluateJavaScript( + arg_instanceId!, arg_javaScriptString!); + return {'result': output}; + }); + } + } + } +} + +class _TestWKUIDelegateHostApiCodec extends StandardMessageCodec { + const _TestWKUIDelegateHostApiCodec(); +} + +abstract class TestWKUIDelegateHostApi { + static const MessageCodec codec = _TestWKUIDelegateHostApiCodec(); + + void create(int instanceId); + static void setup(TestWKUIDelegateHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUIDelegateHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateHostApi.create was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateHostApi.create was null, expected non-null int.'); + api.create(arg_instanceId!); + return {}; + }); + } + } + } +} + +class _TestWKHttpCookieStoreHostApiCodec extends StandardMessageCodec { + const _TestWKHttpCookieStoreHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NSHttpCookieData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is NSHttpCookiePropertyKeyEnumData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NSHttpCookieData.decode(readValue(buffer)!); + + case 129: + return NSHttpCookiePropertyKeyEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestWKHttpCookieStoreHostApi { + static const MessageCodec codec = + _TestWKHttpCookieStoreHostApiCodec(); + + void createFromWebsiteDataStore( + int instanceId, int websiteDataStoreInstanceId); + void setCookie(int instanceId, NSHttpCookieData cookie); + static void setup(TestWKHttpCookieStoreHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKHttpCookieStoreHostApi.createFromWebsiteDataStore', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKHttpCookieStoreHostApi.createFromWebsiteDataStore was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKHttpCookieStoreHostApi.createFromWebsiteDataStore was null, expected non-null int.'); + final int? arg_websiteDataStoreInstanceId = (args[1] as int?); + assert(arg_websiteDataStoreInstanceId != null, + 'Argument for dev.flutter.pigeon.WKHttpCookieStoreHostApi.createFromWebsiteDataStore was null, expected non-null int.'); + api.createFromWebsiteDataStore( + arg_instanceId!, arg_websiteDataStoreInstanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKHttpCookieStoreHostApi.setCookie', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKHttpCookieStoreHostApi.setCookie was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WKHttpCookieStoreHostApi.setCookie was null, expected non-null int.'); + final NSHttpCookieData? arg_cookie = (args[1] as NSHttpCookieData?); + assert(arg_cookie != null, + 'Argument for dev.flutter.pigeon.WKHttpCookieStoreHostApi.setCookie was null, expected non-null NSHttpCookieData.'); + api.setCookie(arg_instanceId!, arg_cookie!); + return {}; + }); + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart new file mode 100644 index 000000000000..007c2bc32252 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart @@ -0,0 +1,100 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; +import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart'; +import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; + +import '../common/test_web_kit.pigeon.dart'; +import 'foundation_test.mocks.dart'; + +@GenerateMocks([ + TestNSObjectHostApi, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Foundation', () { + late InstanceManager instanceManager; + + setUp(() { + instanceManager = InstanceManager(); + }); + + group('NSObject', () { + late MockTestNSObjectHostApi mockPlatformHostApi; + + late NSObject object; + + setUp(() { + mockPlatformHostApi = MockTestNSObjectHostApi(); + TestNSObjectHostApi.setup(mockPlatformHostApi); + + object = NSObject(instanceManager: instanceManager); + instanceManager.tryAddInstance(object); + }); + + tearDown(() { + TestNSObjectHostApi.setup(null); + }); + + test('addObserver', () async { + final NSObject observer = NSObject(instanceManager: instanceManager); + instanceManager.tryAddInstance(observer); + + await object.addObserver( + observer, + keyPath: 'aKeyPath', + options: { + NSKeyValueObservingOptions.initialValue, + NSKeyValueObservingOptions.priorNotification, + }, + ); + + final List optionsData = + verify(mockPlatformHostApi.addObserver( + instanceManager.getInstanceId(object), + instanceManager.getInstanceId(observer), + 'aKeyPath', + captureAny, + )).captured.single as List; + + expect(optionsData, hasLength(2)); + expect( + optionsData[0]!.value, + NSKeyValueObservingOptionsEnum.initialValue, + ); + expect( + optionsData[1]!.value, + NSKeyValueObservingOptionsEnum.priorNotification, + ); + }); + + test('removeObserver', () async { + final NSObject observer = NSObject(instanceManager: instanceManager); + instanceManager.tryAddInstance(observer); + + await object.removeObserver(observer, keyPath: 'aKeyPath'); + + verify(mockPlatformHostApi.removeObserver( + instanceManager.getInstanceId(object), + instanceManager.getInstanceId(observer), + 'aKeyPath', + )); + }); + + test('dispose', () async { + final int instanceId = instanceManager.getInstanceId(object)!; + + await object.dispose(); + verify( + mockPlatformHostApi.dispose(instanceId), + ); + }); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart new file mode 100644 index 000000000000..7bd208eeac05 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart @@ -0,0 +1,48 @@ +// Mocks generated by Mockito 5.1.0 from annotations +// in webview_flutter_wkwebview/example/ios/.symlinks/plugins/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart. +// Do not manually edit this file. + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart' + as _i3; + +import '../common/test_web_kit.pigeon.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +/// A class which mocks [TestNSObjectHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestNSObjectHostApi extends _i1.Mock + implements _i2.TestNSObjectHostApi { + MockTestNSObjectHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void dispose(int? instanceId) => + super.noSuchMethod(Invocation.method(#dispose, [instanceId]), + returnValueForMissingStub: null); + @override + void addObserver(int? instanceId, int? observerInstanceId, String? keyPath, + List<_i3.NSKeyValueObservingOptionsEnumData?>? options) => + super.noSuchMethod( + Invocation.method( + #addObserver, [instanceId, observerInstanceId, keyPath, options]), + returnValueForMissingStub: null); + @override + void removeObserver( + int? instanceId, int? observerInstanceId, String? keyPath) => + super.noSuchMethod( + Invocation.method( + #removeObserver, [instanceId, observerInstanceId, keyPath]), + returnValueForMissingStub: null); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart new file mode 100644 index 000000000000..7db190f8192c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart @@ -0,0 +1,122 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; +import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart'; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; + +import '../common/test_web_kit.pigeon.dart'; +import 'ui_kit_test.mocks.dart'; + +@GenerateMocks([ + TestWKWebViewConfigurationHostApi, + TestWKWebViewHostApi, + TestUIScrollViewHostApi, + TestUIViewHostApi, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('UIKit', () { + late InstanceManager instanceManager; + + setUp(() { + instanceManager = InstanceManager(); + }); + + group('UIScrollView', () { + late MockTestUIScrollViewHostApi mockPlatformHostApi; + + late UIScrollView scrollView; + late int scrollViewInstanceId; + + setUp(() { + mockPlatformHostApi = MockTestUIScrollViewHostApi(); + TestUIScrollViewHostApi.setup(mockPlatformHostApi); + + TestWKWebViewConfigurationHostApi.setup( + MockTestWKWebViewConfigurationHostApi(), + ); + TestWKWebViewHostApi.setup(MockTestWKWebViewHostApi()); + final WKWebView webView = WKWebView( + WKWebViewConfiguration(instanceManager: instanceManager), + instanceManager: instanceManager, + ); + + scrollView = UIScrollView.fromWebView( + webView, + instanceManager: instanceManager, + ); + scrollViewInstanceId = instanceManager.getInstanceId(scrollView)!; + }); + + tearDown(() { + TestUIScrollViewHostApi.setup(null); + TestWKWebViewConfigurationHostApi.setup(null); + TestWKWebViewHostApi.setup(null); + }); + + test('getContentOffset', () async { + when(mockPlatformHostApi.getContentOffset(scrollViewInstanceId)) + .thenReturn([4.0, 10.0]); + expect( + scrollView.getContentOffset(), + completion(const Point(4.0, 10.0)), + ); + }); + + test('scrollBy', () async { + await scrollView.scrollBy(const Point(4.0, 10.0)); + verify(mockPlatformHostApi.scrollBy(scrollViewInstanceId, 4.0, 10.0)); + }); + + test('setContentOffset', () async { + await scrollView.setContentOffset(const Point(4.0, 10.0)); + verify(mockPlatformHostApi.setContentOffset( + scrollViewInstanceId, + 4.0, + 10.0, + )); + }); + }); + + group('UIView', () { + late MockTestUIViewHostApi mockPlatformHostApi; + + late UIView view; + late int viewInstanceId; + + setUp(() { + mockPlatformHostApi = MockTestUIViewHostApi(); + TestUIViewHostApi.setup(mockPlatformHostApi); + + view = UIView(instanceManager: instanceManager); + viewInstanceId = instanceManager.tryAddInstance(view)!; + }); + + tearDown(() { + TestUIViewHostApi.setup(null); + }); + + test('setBackgroundColor', () async { + await view.setBackgroundColor(Colors.red); + verify(mockPlatformHostApi.setBackgroundColor( + viewInstanceId, + Colors.red.value, + )); + }); + + test('setOpaque', () async { + await view.setOpaque(false); + verify(mockPlatformHostApi.setOpaque(viewInstanceId, false)); + }); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart new file mode 100644 index 000000000000..908709c90134 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart @@ -0,0 +1,200 @@ +// Mocks generated by Mockito 5.1.0 from annotations +// in webview_flutter_wkwebview/example/ios/.symlinks/plugins/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i4; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart' + as _i3; + +import '../common/test_web_kit.pigeon.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +/// A class which mocks [TestWKWebViewConfigurationHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKWebViewConfigurationHostApi extends _i1.Mock + implements _i2.TestWKWebViewConfigurationHostApi { + MockTestWKWebViewConfigurationHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId) => + super.noSuchMethod(Invocation.method(#create, [instanceId]), + returnValueForMissingStub: null); + @override + void createFromWebView(int? instanceId, int? webViewInstanceId) => + super.noSuchMethod( + Invocation.method( + #createFromWebView, [instanceId, webViewInstanceId]), + returnValueForMissingStub: null); + @override + void setAllowsInlineMediaPlayback(int? instanceId, bool? allow) => + super.noSuchMethod( + Invocation.method(#setAllowsInlineMediaPlayback, [instanceId, allow]), + returnValueForMissingStub: null); + @override + void setMediaTypesRequiringUserActionForPlayback( + int? instanceId, List<_i3.WKAudiovisualMediaTypeEnumData?>? types) => + super.noSuchMethod( + Invocation.method(#setMediaTypesRequiringUserActionForPlayback, + [instanceId, types]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestWKWebViewHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKWebViewHostApi extends _i1.Mock + implements _i2.TestWKWebViewHostApi { + MockTestWKWebViewHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, int? configurationInstanceId) => + super.noSuchMethod( + Invocation.method(#create, [instanceId, configurationInstanceId]), + returnValueForMissingStub: null); + @override + void setUIDelegate(int? instanceId, int? uiDelegateInstanceId) => + super.noSuchMethod( + Invocation.method(#setUIDelegate, [instanceId, uiDelegateInstanceId]), + returnValueForMissingStub: null); + @override + void setNavigationDelegate( + int? instanceId, int? navigationDelegateInstanceId) => + super.noSuchMethod( + Invocation.method(#setNavigationDelegate, + [instanceId, navigationDelegateInstanceId]), + returnValueForMissingStub: null); + @override + String? getUrl(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getUrl, [instanceId])) as String?); + @override + double getEstimatedProgress(int? instanceId) => (super.noSuchMethod( + Invocation.method(#getEstimatedProgress, [instanceId]), + returnValue: 0.0) as double); + @override + void loadRequest(int? instanceId, _i3.NSUrlRequestData? request) => + super.noSuchMethod(Invocation.method(#loadRequest, [instanceId, request]), + returnValueForMissingStub: null); + @override + void loadHtmlString(int? instanceId, String? string, String? baseUrl) => + super.noSuchMethod( + Invocation.method(#loadHtmlString, [instanceId, string, baseUrl]), + returnValueForMissingStub: null); + @override + void loadFileUrl(int? instanceId, String? url, String? readAccessUrl) => + super.noSuchMethod( + Invocation.method(#loadFileUrl, [instanceId, url, readAccessUrl]), + returnValueForMissingStub: null); + @override + void loadFlutterAsset(int? instanceId, String? key) => super.noSuchMethod( + Invocation.method(#loadFlutterAsset, [instanceId, key]), + returnValueForMissingStub: null); + @override + bool canGoBack(int? instanceId) => + (super.noSuchMethod(Invocation.method(#canGoBack, [instanceId]), + returnValue: false) as bool); + @override + bool canGoForward(int? instanceId) => + (super.noSuchMethod(Invocation.method(#canGoForward, [instanceId]), + returnValue: false) as bool); + @override + void goBack(int? instanceId) => + super.noSuchMethod(Invocation.method(#goBack, [instanceId]), + returnValueForMissingStub: null); + @override + void goForward(int? instanceId) => + super.noSuchMethod(Invocation.method(#goForward, [instanceId]), + returnValueForMissingStub: null); + @override + void reload(int? instanceId) => + super.noSuchMethod(Invocation.method(#reload, [instanceId]), + returnValueForMissingStub: null); + @override + String? getTitle(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getTitle, [instanceId])) + as String?); + @override + void setAllowsBackForwardNavigationGestures(int? instanceId, bool? allow) => + super.noSuchMethod( + Invocation.method( + #setAllowsBackForwardNavigationGestures, [instanceId, allow]), + returnValueForMissingStub: null); + @override + void setCustomUserAgent(int? instanceId, String? userAgent) => + super.noSuchMethod( + Invocation.method(#setCustomUserAgent, [instanceId, userAgent]), + returnValueForMissingStub: null); + @override + _i4.Future evaluateJavaScript( + int? instanceId, String? javaScriptString) => + (super.noSuchMethod( + Invocation.method( + #evaluateJavaScript, [instanceId, javaScriptString]), + returnValue: Future.value()) as _i4.Future); +} + +/// A class which mocks [TestUIScrollViewHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestUIScrollViewHostApi extends _i1.Mock + implements _i2.TestUIScrollViewHostApi { + MockTestUIScrollViewHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void createFromWebView(int? instanceId, int? webViewInstanceId) => + super.noSuchMethod( + Invocation.method( + #createFromWebView, [instanceId, webViewInstanceId]), + returnValueForMissingStub: null); + @override + List getContentOffset(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getContentOffset, [instanceId]), + returnValue: []) as List); + @override + void scrollBy(int? instanceId, double? x, double? y) => + super.noSuchMethod(Invocation.method(#scrollBy, [instanceId, x, y]), + returnValueForMissingStub: null); + @override + void setContentOffset(int? instanceId, double? x, double? y) => super + .noSuchMethod(Invocation.method(#setContentOffset, [instanceId, x, y]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestUIViewHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestUIViewHostApi extends _i1.Mock implements _i2.TestUIViewHostApi { + MockTestUIViewHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + List getContentOffset(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getContentOffset, [instanceId]), + returnValue: []) as List); + @override + void setBackgroundColor(int? instanceId, int? value) => super.noSuchMethod( + Invocation.method(#setBackgroundColor, [instanceId, value]), + returnValueForMissingStub: null); + @override + void setOpaque(int? instanceId, bool? opaque) => + super.noSuchMethod(Invocation.method(#setOpaque, [instanceId, opaque]), + returnValueForMissingStub: null); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart new file mode 100644 index 000000000000..8564bf889c2f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart @@ -0,0 +1,658 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; +import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart'; +import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit_api_impls.dart'; + +import '../common/test_web_kit.pigeon.dart'; +import 'web_kit_test.mocks.dart'; + +@GenerateMocks([ + TestWKHttpCookieStoreHostApi, + TestWKNavigationDelegateHostApi, + TestWKPreferencesHostApi, + TestWKScriptMessageHandlerHostApi, + TestWKUIDelegateHostApi, + TestWKUserContentControllerHostApi, + TestWKWebViewConfigurationHostApi, + TestWKWebViewHostApi, + TestWKWebsiteDataStoreHostApi, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('WebKit', () { + late InstanceManager instanceManager; + late WebKitFlutterApis flutterApis; + + setUp(() { + instanceManager = InstanceManager(); + flutterApis = WebKitFlutterApis(instanceManager: instanceManager); + WebKitFlutterApis.instance = flutterApis; + }); + + group('WKWebsiteDataStore', () { + late MockTestWKWebsiteDataStoreHostApi mockPlatformHostApi; + + late WKWebsiteDataStore websiteDataStore; + + late WKWebViewConfiguration webViewConfiguration; + + setUp(() { + mockPlatformHostApi = MockTestWKWebsiteDataStoreHostApi(); + TestWKWebsiteDataStoreHostApi.setup(mockPlatformHostApi); + + TestWKWebViewConfigurationHostApi.setup( + MockTestWKWebViewConfigurationHostApi(), + ); + webViewConfiguration = WKWebViewConfiguration( + instanceManager: instanceManager, + ); + + websiteDataStore = WKWebsiteDataStore.fromWebViewConfiguration( + webViewConfiguration, + instanceManager: instanceManager, + ); + }); + + tearDown(() { + TestWKWebsiteDataStoreHostApi.setup(null); + TestWKWebViewConfigurationHostApi.setup(null); + }); + + test('createFromWebViewConfiguration', () { + verify(mockPlatformHostApi.createFromWebViewConfiguration( + instanceManager.getInstanceId(websiteDataStore), + instanceManager.getInstanceId(webViewConfiguration), + )); + }); + + test('createDefaultDataStore', () { + final WKWebsiteDataStore defaultDataStore = + WKWebsiteDataStore.defaultDataStore; + verify( + mockPlatformHostApi.createDefaultDataStore( + InstanceManager.instance.getInstanceId(defaultDataStore), + ), + ); + }); + + test('removeDataOfTypes', () { + when(mockPlatformHostApi.removeDataOfTypes( + any, + any, + any, + )).thenAnswer((_) => Future.value(true)); + + expect( + websiteDataStore.removeDataOfTypes( + {WKWebsiteDataTypes.cookies}, + DateTime.fromMillisecondsSinceEpoch(5000), + ), + completion(true), + ); + + final List typeData = + verify(mockPlatformHostApi.removeDataOfTypes( + instanceManager.getInstanceId(websiteDataStore), + captureAny, + 5.0, + )).captured.single.cast() + as List; + + expect(typeData.single.value, WKWebsiteDataTypesEnum.cookies); + }); + }); + + group('WKHttpCookieStore', () { + late MockTestWKHttpCookieStoreHostApi mockPlatformHostApi; + + late WKHttpCookieStore httpCookieStore; + + late WKWebsiteDataStore websiteDataStore; + + setUp(() { + mockPlatformHostApi = MockTestWKHttpCookieStoreHostApi(); + TestWKHttpCookieStoreHostApi.setup(mockPlatformHostApi); + + TestWKWebViewConfigurationHostApi.setup( + MockTestWKWebViewConfigurationHostApi(), + ); + TestWKWebsiteDataStoreHostApi.setup( + MockTestWKWebsiteDataStoreHostApi(), + ); + + websiteDataStore = WKWebsiteDataStore.fromWebViewConfiguration( + WKWebViewConfiguration(instanceManager: instanceManager), + instanceManager: instanceManager, + ); + + httpCookieStore = WKHttpCookieStore.fromWebsiteDataStore( + websiteDataStore, + instanceManager: instanceManager, + ); + }); + + tearDown(() { + TestWKHttpCookieStoreHostApi.setup(null); + TestWKWebsiteDataStoreHostApi.setup(null); + TestWKWebViewConfigurationHostApi.setup(null); + }); + + test('createFromWebsiteDataStore', () { + verify(mockPlatformHostApi.createFromWebsiteDataStore( + instanceManager.getInstanceId(httpCookieStore), + instanceManager.getInstanceId(websiteDataStore), + )); + }); + + test('setCookie', () async { + await httpCookieStore.setCookie( + const NSHttpCookie.withProperties({ + NSHttpCookiePropertyKey.comment: 'aComment', + })); + + final NSHttpCookieData cookie = verify( + mockPlatformHostApi.setCookie( + instanceManager.getInstanceId(httpCookieStore)!, + captureAny, + ), + ).captured.single as NSHttpCookieData; + + expect( + cookie.properties.entries.single.key!.value, + NSHttpCookiePropertyKeyEnum.comment, + ); + expect(cookie.properties.entries.single.value, 'aComment'); + }); + }); + + group('WKScriptMessageHandler', () { + late MockTestWKScriptMessageHandlerHostApi mockPlatformHostApi; + + late WKScriptMessageHandler scriptMessageHandler; + + setUp(() async { + mockPlatformHostApi = MockTestWKScriptMessageHandlerHostApi(); + TestWKScriptMessageHandlerHostApi.setup(mockPlatformHostApi); + + scriptMessageHandler = WKScriptMessageHandler( + instanceManager: instanceManager, + ); + }); + + tearDown(() { + TestWKScriptMessageHandlerHostApi.setup(null); + }); + + test('create', () async { + verify(mockPlatformHostApi.create( + instanceManager.getInstanceId(scriptMessageHandler), + )); + }); + }); + + group('WKPreferences', () { + late MockTestWKPreferencesHostApi mockPlatformHostApi; + + late WKPreferences preferences; + + late WKWebViewConfiguration webViewConfiguration; + + setUp(() { + mockPlatformHostApi = MockTestWKPreferencesHostApi(); + TestWKPreferencesHostApi.setup(mockPlatformHostApi); + + TestWKWebViewConfigurationHostApi.setup( + MockTestWKWebViewConfigurationHostApi(), + ); + webViewConfiguration = WKWebViewConfiguration( + instanceManager: instanceManager, + ); + + preferences = WKPreferences.fromWebViewConfiguration( + webViewConfiguration, + instanceManager: instanceManager, + ); + }); + + tearDown(() { + TestWKPreferencesHostApi.setup(null); + TestWKWebViewConfigurationHostApi.setup(null); + }); + + test('createFromWebViewConfiguration', () async { + verify(mockPlatformHostApi.createFromWebViewConfiguration( + instanceManager.getInstanceId(preferences), + instanceManager.getInstanceId(webViewConfiguration), + )); + }); + + test('setJavaScriptEnabled', () async { + await preferences.setJavaScriptEnabled(true); + verify(mockPlatformHostApi.setJavaScriptEnabled( + instanceManager.getInstanceId(preferences), + true, + )); + }); + }); + + group('WKUserContentController', () { + late MockTestWKUserContentControllerHostApi mockPlatformHostApi; + + late WKUserContentController userContentController; + + late WKWebViewConfiguration webViewConfiguration; + + setUp(() { + mockPlatformHostApi = MockTestWKUserContentControllerHostApi(); + TestWKUserContentControllerHostApi.setup(mockPlatformHostApi); + + TestWKWebViewConfigurationHostApi.setup( + MockTestWKWebViewConfigurationHostApi(), + ); + webViewConfiguration = WKWebViewConfiguration( + instanceManager: instanceManager, + ); + + userContentController = + WKUserContentController.fromWebViewConfiguration( + webViewConfiguration, + instanceManager: instanceManager, + ); + }); + + tearDown(() { + TestWKUserContentControllerHostApi.setup(null); + TestWKWebViewConfigurationHostApi.setup(null); + }); + + test('createFromWebViewConfiguration', () async { + verify(mockPlatformHostApi.createFromWebViewConfiguration( + instanceManager.getInstanceId(userContentController), + instanceManager.getInstanceId(webViewConfiguration), + )); + }); + + test('addScriptMessageHandler', () async { + TestWKScriptMessageHandlerHostApi.setup( + MockTestWKScriptMessageHandlerHostApi(), + ); + final WKScriptMessageHandler handler = WKScriptMessageHandler( + instanceManager: instanceManager, + ); + + userContentController.addScriptMessageHandler(handler, 'handlerName'); + verify(mockPlatformHostApi.addScriptMessageHandler( + instanceManager.getInstanceId(userContentController), + instanceManager.getInstanceId(handler), + 'handlerName', + )); + }); + + test('removeScriptMessageHandler', () async { + userContentController.removeScriptMessageHandler('handlerName'); + verify(mockPlatformHostApi.removeScriptMessageHandler( + instanceManager.getInstanceId(userContentController), + 'handlerName', + )); + }); + + test('removeAllScriptMessageHandlers', () async { + userContentController.removeAllScriptMessageHandlers(); + verify(mockPlatformHostApi.removeAllScriptMessageHandlers( + instanceManager.getInstanceId(userContentController), + )); + }); + + test('addUserScript', () { + userContentController.addUserScript(const WKUserScript( + 'aScript', + WKUserScriptInjectionTime.atDocumentEnd, + isMainFrameOnly: false, + )); + verify(mockPlatformHostApi.addUserScript( + instanceManager.getInstanceId(userContentController), + argThat(isA()), + )); + }); + + test('removeAllUserScripts', () { + userContentController.removeAllUserScripts(); + verify(mockPlatformHostApi.removeAllUserScripts( + instanceManager.getInstanceId(userContentController), + )); + }); + }); + + group('WKWebViewConfiguration', () { + late MockTestWKWebViewConfigurationHostApi mockPlatformHostApi; + + late WKWebViewConfiguration webViewConfiguration; + + setUp(() async { + mockPlatformHostApi = MockTestWKWebViewConfigurationHostApi(); + TestWKWebViewConfigurationHostApi.setup(mockPlatformHostApi); + + webViewConfiguration = WKWebViewConfiguration( + instanceManager: instanceManager, + ); + }); + + tearDown(() { + TestWKWebViewConfigurationHostApi.setup(null); + }); + + test('create', () async { + verify( + mockPlatformHostApi.create(instanceManager.getInstanceId( + webViewConfiguration, + )), + ); + }); + + test('createFromWebView', () async { + TestWKWebViewHostApi.setup(MockTestWKWebViewHostApi()); + final WKWebView webView = WKWebView( + webViewConfiguration, + instanceManager: instanceManager, + ); + + final WKWebViewConfiguration configurationFromWebView = + WKWebViewConfiguration.fromWebView( + webView, + instanceManager: instanceManager, + ); + verify(mockPlatformHostApi.createFromWebView( + instanceManager.getInstanceId(configurationFromWebView)!, + instanceManager.getInstanceId(webView)!, + )); + }); + + test('allowsInlineMediaPlayback', () { + webViewConfiguration.setAllowsInlineMediaPlayback(true); + verify(mockPlatformHostApi.setAllowsInlineMediaPlayback( + instanceManager.getInstanceId(webViewConfiguration), + true, + )); + }); + + test('mediaTypesRequiringUserActionForPlayback', () { + webViewConfiguration.setMediaTypesRequiringUserActionForPlayback( + { + WKAudiovisualMediaType.audio, + WKAudiovisualMediaType.video, + }, + ); + + final List typeData = verify( + mockPlatformHostApi.setMediaTypesRequiringUserActionForPlayback( + instanceManager.getInstanceId(webViewConfiguration), + captureAny, + )).captured.single as List; + + expect(typeData, hasLength(2)); + expect(typeData[0]!.value, WKAudiovisualMediaTypeEnum.audio); + expect(typeData[1]!.value, WKAudiovisualMediaTypeEnum.video); + }); + }); + + group('WKNavigationDelegate', () { + late MockTestWKNavigationDelegateHostApi mockPlatformHostApi; + + late WKWebView webView; + + late WKNavigationDelegate navigationDelegate; + + setUp(() async { + mockPlatformHostApi = MockTestWKNavigationDelegateHostApi(); + TestWKNavigationDelegateHostApi.setup(mockPlatformHostApi); + + TestWKWebViewConfigurationHostApi.setup( + MockTestWKWebViewConfigurationHostApi(), + ); + TestWKWebViewHostApi.setup(MockTestWKWebViewHostApi()); + webView = WKWebView( + WKWebViewConfiguration(instanceManager: instanceManager), + instanceManager: instanceManager, + ); + + navigationDelegate = WKNavigationDelegate( + instanceManager: instanceManager, + ); + }); + + tearDown(() { + TestWKNavigationDelegateHostApi.setup(null); + TestWKWebViewConfigurationHostApi.setup(null); + TestWKWebViewHostApi.setup(null); + }); + + test('create', () async { + verify(mockPlatformHostApi.create( + instanceManager.getInstanceId(navigationDelegate), + )); + }); + + test('setDidFinishNavigation', () async { + final Completer> argsCompleter = + Completer>(); + + navigationDelegate.setDidFinishNavigation( + (WKWebView webView, String? url) { + argsCompleter.complete([webView, url]); + }, + ); + + final int functionInstanceId = + verify(mockPlatformHostApi.setDidFinishNavigation( + instanceManager.getInstanceId(navigationDelegate), + captureAny, + )).captured.single as int; + + flutterApis.navigationDelegateFlutterApi.didFinishNavigation( + functionInstanceId, + instanceManager.getInstanceId(webView)!, + 'url', + ); + + expect(argsCompleter.future, completion([webView, 'url'])); + }); + }); + + group('WKWebView', () { + late MockTestWKWebViewHostApi mockPlatformHostApi; + + late WKWebViewConfiguration webViewConfiguration; + + late WKWebView webView; + late int webViewInstanceId; + + setUp(() { + mockPlatformHostApi = MockTestWKWebViewHostApi(); + TestWKWebViewHostApi.setup(mockPlatformHostApi); + + TestWKWebViewConfigurationHostApi.setup( + MockTestWKWebViewConfigurationHostApi()); + webViewConfiguration = WKWebViewConfiguration( + instanceManager: instanceManager, + ); + + webView = WKWebView( + webViewConfiguration, + instanceManager: instanceManager, + ); + webViewInstanceId = instanceManager.getInstanceId(webView)!; + }); + + tearDown(() { + TestWKWebViewHostApi.setup(null); + TestWKWebViewConfigurationHostApi.setup(null); + }); + + test('create', () async { + verify(mockPlatformHostApi.create( + instanceManager.getInstanceId(webView), + instanceManager.getInstanceId( + webViewConfiguration, + ), + )); + }); + + test('setUIDelegate', () async { + TestWKUIDelegateHostApi.setup(MockTestWKUIDelegateHostApi()); + final WKUIDelegate uiDelegate = WKUIDelegate( + instanceManager: instanceManager, + ); + + await webView.setUIDelegate(uiDelegate); + verify(mockPlatformHostApi.setUIDelegate( + webViewInstanceId, + instanceManager.getInstanceId(uiDelegate), + )); + + TestWKUIDelegateHostApi.setup(null); + }); + + test('setNavigationDelegate', () async { + TestWKNavigationDelegateHostApi.setup( + MockTestWKNavigationDelegateHostApi(), + ); + final WKNavigationDelegate navigationDelegate = WKNavigationDelegate( + instanceManager: instanceManager, + ); + + await webView.setNavigationDelegate(navigationDelegate); + verify(mockPlatformHostApi.setNavigationDelegate( + webViewInstanceId, + instanceManager.getInstanceId(navigationDelegate), + )); + + TestWKNavigationDelegateHostApi.setup(null); + }); + + test('getUrl', () { + when( + mockPlatformHostApi.getUrl(webViewInstanceId), + ).thenReturn('www.flutter.dev'); + expect(webView.getUrl(), completion('www.flutter.dev')); + }); + + test('getEstimatedProgress', () { + when( + mockPlatformHostApi.getEstimatedProgress(webViewInstanceId), + ).thenReturn(54.5); + expect(webView.getEstimatedProgress(), completion(54.5)); + }); + + test('loadRequest', () { + webView.loadRequest(const NSUrlRequest(url: 'www.flutter.dev')); + verify(mockPlatformHostApi.loadRequest( + webViewInstanceId, + argThat(isA()), + )); + }); + + test('loadHtmlString', () { + webView.loadHtmlString('a', baseUrl: 'b'); + verify(mockPlatformHostApi.loadHtmlString(webViewInstanceId, 'a', 'b')); + }); + + test('loadFileUrl', () { + webView.loadFileUrl('a', readAccessUrl: 'b'); + verify(mockPlatformHostApi.loadFileUrl(webViewInstanceId, 'a', 'b')); + }); + + test('loadFlutterAsset', () { + webView.loadFlutterAsset('a'); + verify(mockPlatformHostApi.loadFlutterAsset(webViewInstanceId, 'a')); + }); + + test('canGoBack', () { + when(mockPlatformHostApi.canGoBack(webViewInstanceId)).thenReturn(true); + expect(webView.canGoBack(), completion(isTrue)); + }); + + test('canGoForward', () { + when(mockPlatformHostApi.canGoForward(webViewInstanceId)) + .thenReturn(false); + expect(webView.canGoForward(), completion(isFalse)); + }); + + test('goBack', () { + webView.goBack(); + verify(mockPlatformHostApi.goBack(webViewInstanceId)); + }); + + test('goForward', () { + webView.goForward(); + verify(mockPlatformHostApi.goForward(webViewInstanceId)); + }); + + test('reload', () { + webView.reload(); + verify(mockPlatformHostApi.reload(webViewInstanceId)); + }); + + test('getTitle', () { + when(mockPlatformHostApi.getTitle(webViewInstanceId)) + .thenReturn('MyTitle'); + expect(webView.getTitle(), completion('MyTitle')); + }); + + test('setAllowsBackForwardNavigationGestures', () { + webView.setAllowsBackForwardNavigationGestures(false); + verify(mockPlatformHostApi.setAllowsBackForwardNavigationGestures( + webViewInstanceId, + false, + )); + }); + + test('customUserAgent', () { + webView.setCustomUserAgent('hello'); + verify(mockPlatformHostApi.setCustomUserAgent( + webViewInstanceId, + 'hello', + )); + }); + + test('evaluateJavaScript', () { + when(mockPlatformHostApi.evaluateJavaScript(webViewInstanceId, 'gogo')) + .thenAnswer((_) => Future.value('stopstop')); + expect(webView.evaluateJavaScript('gogo'), completion('stopstop')); + }); + }); + + group('WKUIDelegate', () { + late MockTestWKUIDelegateHostApi mockPlatformHostApi; + + late WKUIDelegate uiDelegate; + + setUp(() async { + mockPlatformHostApi = MockTestWKUIDelegateHostApi(); + TestWKUIDelegateHostApi.setup(mockPlatformHostApi); + + uiDelegate = WKUIDelegate(instanceManager: instanceManager); + }); + + tearDown(() { + TestWKUIDelegateHostApi.setup(null); + }); + + test('create', () async { + verify(mockPlatformHostApi.create( + instanceManager.getInstanceId(uiDelegate), + )); + }); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart new file mode 100644 index 000000000000..d4f1fefc190c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart @@ -0,0 +1,318 @@ +// Mocks generated by Mockito 5.1.0 from annotations +// in webview_flutter_wkwebview/example/ios/.symlinks/plugins/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i4; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart' + as _i3; + +import '../common/test_web_kit.pigeon.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +/// A class which mocks [TestWKHttpCookieStoreHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKHttpCookieStoreHostApi extends _i1.Mock + implements _i2.TestWKHttpCookieStoreHostApi { + MockTestWKHttpCookieStoreHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void createFromWebsiteDataStore( + int? instanceId, int? websiteDataStoreInstanceId) => + super.noSuchMethod( + Invocation.method(#createFromWebsiteDataStore, + [instanceId, websiteDataStoreInstanceId]), + returnValueForMissingStub: null); + @override + void setCookie(int? instanceId, _i3.NSHttpCookieData? cookie) => + super.noSuchMethod(Invocation.method(#setCookie, [instanceId, cookie]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestWKNavigationDelegateHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKNavigationDelegateHostApi extends _i1.Mock + implements _i2.TestWKNavigationDelegateHostApi { + MockTestWKNavigationDelegateHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId) => + super.noSuchMethod(Invocation.method(#create, [instanceId]), + returnValueForMissingStub: null); + @override + void setDidFinishNavigation(int? instanceId, int? functionInstanceId) => + super.noSuchMethod( + Invocation.method( + #setDidFinishNavigation, [instanceId, functionInstanceId]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestWKPreferencesHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKPreferencesHostApi extends _i1.Mock + implements _i2.TestWKPreferencesHostApi { + MockTestWKPreferencesHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void createFromWebViewConfiguration( + int? instanceId, int? configurationInstanceId) => + super.noSuchMethod( + Invocation.method(#createFromWebViewConfiguration, + [instanceId, configurationInstanceId]), + returnValueForMissingStub: null); + @override + void setJavaScriptEnabled(int? instanceId, bool? enabled) => + super.noSuchMethod( + Invocation.method(#setJavaScriptEnabled, [instanceId, enabled]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestWKScriptMessageHandlerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKScriptMessageHandlerHostApi extends _i1.Mock + implements _i2.TestWKScriptMessageHandlerHostApi { + MockTestWKScriptMessageHandlerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId) => + super.noSuchMethod(Invocation.method(#create, [instanceId]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestWKUIDelegateHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKUIDelegateHostApi extends _i1.Mock + implements _i2.TestWKUIDelegateHostApi { + MockTestWKUIDelegateHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId) => + super.noSuchMethod(Invocation.method(#create, [instanceId]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestWKUserContentControllerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKUserContentControllerHostApi extends _i1.Mock + implements _i2.TestWKUserContentControllerHostApi { + MockTestWKUserContentControllerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void createFromWebViewConfiguration( + int? instanceId, int? configurationInstanceId) => + super.noSuchMethod( + Invocation.method(#createFromWebViewConfiguration, + [instanceId, configurationInstanceId]), + returnValueForMissingStub: null); + @override + void addScriptMessageHandler( + int? instanceId, int? handlerInstanceid, String? name) => + super.noSuchMethod( + Invocation.method( + #addScriptMessageHandler, [instanceId, handlerInstanceid, name]), + returnValueForMissingStub: null); + @override + void removeScriptMessageHandler(int? instanceId, String? name) => + super.noSuchMethod( + Invocation.method(#removeScriptMessageHandler, [instanceId, name]), + returnValueForMissingStub: null); + @override + void removeAllScriptMessageHandlers(int? instanceId) => super.noSuchMethod( + Invocation.method(#removeAllScriptMessageHandlers, [instanceId]), + returnValueForMissingStub: null); + @override + void addUserScript(int? instanceId, _i3.WKUserScriptData? userScript) => super + .noSuchMethod(Invocation.method(#addUserScript, [instanceId, userScript]), + returnValueForMissingStub: null); + @override + void removeAllUserScripts(int? instanceId) => + super.noSuchMethod(Invocation.method(#removeAllUserScripts, [instanceId]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestWKWebViewConfigurationHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKWebViewConfigurationHostApi extends _i1.Mock + implements _i2.TestWKWebViewConfigurationHostApi { + MockTestWKWebViewConfigurationHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId) => + super.noSuchMethod(Invocation.method(#create, [instanceId]), + returnValueForMissingStub: null); + @override + void createFromWebView(int? instanceId, int? webViewInstanceId) => + super.noSuchMethod( + Invocation.method( + #createFromWebView, [instanceId, webViewInstanceId]), + returnValueForMissingStub: null); + @override + void setAllowsInlineMediaPlayback(int? instanceId, bool? allow) => + super.noSuchMethod( + Invocation.method(#setAllowsInlineMediaPlayback, [instanceId, allow]), + returnValueForMissingStub: null); + @override + void setMediaTypesRequiringUserActionForPlayback( + int? instanceId, List<_i3.WKAudiovisualMediaTypeEnumData?>? types) => + super.noSuchMethod( + Invocation.method(#setMediaTypesRequiringUserActionForPlayback, + [instanceId, types]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestWKWebViewHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKWebViewHostApi extends _i1.Mock + implements _i2.TestWKWebViewHostApi { + MockTestWKWebViewHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, int? configurationInstanceId) => + super.noSuchMethod( + Invocation.method(#create, [instanceId, configurationInstanceId]), + returnValueForMissingStub: null); + @override + void setUIDelegate(int? instanceId, int? uiDelegateInstanceId) => + super.noSuchMethod( + Invocation.method(#setUIDelegate, [instanceId, uiDelegateInstanceId]), + returnValueForMissingStub: null); + @override + void setNavigationDelegate( + int? instanceId, int? navigationDelegateInstanceId) => + super.noSuchMethod( + Invocation.method(#setNavigationDelegate, + [instanceId, navigationDelegateInstanceId]), + returnValueForMissingStub: null); + @override + String? getUrl(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getUrl, [instanceId])) as String?); + @override + double getEstimatedProgress(int? instanceId) => (super.noSuchMethod( + Invocation.method(#getEstimatedProgress, [instanceId]), + returnValue: 0.0) as double); + @override + void loadRequest(int? instanceId, _i3.NSUrlRequestData? request) => + super.noSuchMethod(Invocation.method(#loadRequest, [instanceId, request]), + returnValueForMissingStub: null); + @override + void loadHtmlString(int? instanceId, String? string, String? baseUrl) => + super.noSuchMethod( + Invocation.method(#loadHtmlString, [instanceId, string, baseUrl]), + returnValueForMissingStub: null); + @override + void loadFileUrl(int? instanceId, String? url, String? readAccessUrl) => + super.noSuchMethod( + Invocation.method(#loadFileUrl, [instanceId, url, readAccessUrl]), + returnValueForMissingStub: null); + @override + void loadFlutterAsset(int? instanceId, String? key) => super.noSuchMethod( + Invocation.method(#loadFlutterAsset, [instanceId, key]), + returnValueForMissingStub: null); + @override + bool canGoBack(int? instanceId) => + (super.noSuchMethod(Invocation.method(#canGoBack, [instanceId]), + returnValue: false) as bool); + @override + bool canGoForward(int? instanceId) => + (super.noSuchMethod(Invocation.method(#canGoForward, [instanceId]), + returnValue: false) as bool); + @override + void goBack(int? instanceId) => + super.noSuchMethod(Invocation.method(#goBack, [instanceId]), + returnValueForMissingStub: null); + @override + void goForward(int? instanceId) => + super.noSuchMethod(Invocation.method(#goForward, [instanceId]), + returnValueForMissingStub: null); + @override + void reload(int? instanceId) => + super.noSuchMethod(Invocation.method(#reload, [instanceId]), + returnValueForMissingStub: null); + @override + String? getTitle(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getTitle, [instanceId])) + as String?); + @override + void setAllowsBackForwardNavigationGestures(int? instanceId, bool? allow) => + super.noSuchMethod( + Invocation.method( + #setAllowsBackForwardNavigationGestures, [instanceId, allow]), + returnValueForMissingStub: null); + @override + void setCustomUserAgent(int? instanceId, String? userAgent) => + super.noSuchMethod( + Invocation.method(#setCustomUserAgent, [instanceId, userAgent]), + returnValueForMissingStub: null); + @override + _i4.Future evaluateJavaScript( + int? instanceId, String? javaScriptString) => + (super.noSuchMethod( + Invocation.method( + #evaluateJavaScript, [instanceId, javaScriptString]), + returnValue: Future.value()) as _i4.Future); +} + +/// A class which mocks [TestWKWebsiteDataStoreHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKWebsiteDataStoreHostApi extends _i1.Mock + implements _i2.TestWKWebsiteDataStoreHostApi { + MockTestWKWebsiteDataStoreHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void createFromWebViewConfiguration( + int? instanceId, int? configurationInstanceId) => + super.noSuchMethod( + Invocation.method(#createFromWebViewConfiguration, + [instanceId, configurationInstanceId]), + returnValueForMissingStub: null); + @override + void createDefaultDataStore(int? instanceId) => super.noSuchMethod( + Invocation.method(#createDefaultDataStore, [instanceId]), + returnValueForMissingStub: null); + @override + _i4.Future removeDataOfTypes( + int? instanceId, + List<_i3.WKWebsiteDataTypesEnumData?>? dataTypes, + double? secondsModifiedSinceEpoch) => + (super.noSuchMethod( + Invocation.method(#removeDataOfTypes, + [instanceId, dataTypes, secondsModifiedSinceEpoch]), + returnValue: Future.value(false)) as _i4.Future); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.dart new file mode 100644 index 000000000000..8f23ff6b4a3d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.dart @@ -0,0 +1,83 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; +import 'package:webview_flutter_wkwebview/src/web_kit_cookie_manager.dart'; + +import 'web_kit_cookie_manager_test.mocks.dart'; + +@GenerateMocks([ + WKHttpCookieStore, + WKWebsiteDataStore, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('WebKitWebViewWidget', () { + late MockWKWebsiteDataStore mockWebsiteDataStore; + late MockWKHttpCookieStore mockWKHttpCookieStore; + + late WebKitCookieManager cookieManager; + + setUp(() { + mockWebsiteDataStore = MockWKWebsiteDataStore(); + mockWKHttpCookieStore = MockWKHttpCookieStore(); + when(mockWebsiteDataStore.httpCookieStore) + .thenReturn(mockWKHttpCookieStore); + + cookieManager = + WebKitCookieManager(websiteDataStore: mockWebsiteDataStore); + }); + + test('clearCookies', () async { + when(mockWebsiteDataStore.removeDataOfTypes( + {WKWebsiteDataTypes.cookies}, any)) + .thenAnswer((_) => Future.value(true)); + expect(cookieManager.clearCookies(), completion(true)); + + when(mockWebsiteDataStore.removeDataOfTypes( + {WKWebsiteDataTypes.cookies}, any)) + .thenAnswer((_) => Future.value(false)); + expect(cookieManager.clearCookies(), completion(false)); + }); + + test('setCookie', () async { + await cookieManager.setCookie( + const WebViewCookie(name: 'a', value: 'b', domain: 'c', path: 'd'), + ); + + final NSHttpCookie cookie = + verify(mockWKHttpCookieStore.setCookie(captureAny)).captured.single + as NSHttpCookie; + expect( + cookie.properties, + { + NSHttpCookiePropertyKey.name: 'a', + NSHttpCookiePropertyKey.value: 'b', + NSHttpCookiePropertyKey.domain: 'c', + NSHttpCookiePropertyKey.path: 'd', + }, + ); + }); + + test('setCookie throws argument error with invalid path', () async { + expect( + () => cookieManager.setCookie( + WebViewCookie( + name: 'a', + value: 'b', + domain: 'c', + path: String.fromCharCode(0x1F), + ), + ), + throwsArgumentError, + ); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.mocks.dart new file mode 100644 index 000000000000..a85c57f7bdb3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.mocks.dart @@ -0,0 +1,86 @@ +// Mocks generated by Mockito 5.1.0 from annotations +// in webview_flutter_wkwebview/example/ios/.symlinks/plugins/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i3; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart' + as _i4; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +class _FakeWKHttpCookieStore_0 extends _i1.Fake + implements _i2.WKHttpCookieStore {} + +/// A class which mocks [WKHttpCookieStore]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWKHttpCookieStore extends _i1.Mock implements _i2.WKHttpCookieStore { + MockWKHttpCookieStore() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future setCookie(_i4.NSHttpCookie? cookie) => + (super.noSuchMethod(Invocation.method(#setCookie, [cookie]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); + @override + _i3.Future addObserver(_i4.NSObject? observer, + {String? keyPath, Set<_i4.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); + @override + _i3.Future removeObserver(_i4.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); + @override + _i3.Future dispose() => + (super.noSuchMethod(Invocation.method(#dispose, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); + @override + _i3.Future setObserveValue( + void Function( + String, _i4.NSObject, Map<_i4.NSKeyValueChangeKey, Object?>)? + observeValue) => + (super.noSuchMethod(Invocation.method(#setObserveValue, [observeValue]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); +} + +/// A class which mocks [WKWebsiteDataStore]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWKWebsiteDataStore extends _i1.Mock + implements _i2.WKWebsiteDataStore { + MockWKWebsiteDataStore() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WKHttpCookieStore get httpCookieStore => + (super.noSuchMethod(Invocation.getter(#httpCookieStore), + returnValue: _FakeWKHttpCookieStore_0()) as _i2.WKHttpCookieStore); + @override + _i3.Future removeDataOfTypes( + Set<_i2.WKWebsiteDataTypes>? dataTypes, DateTime? since) => + (super.noSuchMethod( + Invocation.method(#removeDataOfTypes, [dataTypes, since]), + returnValue: Future.value(false)) as _i3.Future); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.dart index 6642b4561cfc..e5042dab78e4 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.dart @@ -2,24 +2,30 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; +import 'dart:math'; +import 'dart:typed_data'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; +import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; import 'package:webview_flutter_wkwebview/src/web_kit_webview_widget.dart'; import 'web_kit_webview_widget_test.mocks.dart'; @GenerateMocks([ + UIScrollView, WKNavigationDelegate, + WKPreferences, WKScriptMessageHandler, WKWebView, WKWebViewConfiguration, + WKWebsiteDataStore, WKUIDelegate, WKUserContentController, JavascriptChannelRegistry, @@ -29,12 +35,15 @@ import 'web_kit_webview_widget_test.mocks.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('$WebKitWebViewWidget', () { + group('WebKitWebViewWidget', () { late MockWKWebView mockWebView; late MockWebViewWidgetProxy mockWebViewWidgetProxy; late MockWKUserContentController mockUserContentController; + late MockWKPreferences mockPreferences; late MockWKWebViewConfiguration mockWebViewConfiguration; late MockWKUIDelegate mockUIDelegate; + late MockUIScrollView mockScrollView; + late MockWKWebsiteDataStore mockWebsiteDataStore; late MockWKNavigationDelegate mockNavigationDelegate; late MockWebViewPlatformCallbacksHandler mockCallbacksHandler; @@ -46,7 +55,10 @@ void main() { mockWebView = MockWKWebView(); mockWebViewConfiguration = MockWKWebViewConfiguration(); mockUserContentController = MockWKUserContentController(); + mockPreferences = MockWKPreferences(); mockUIDelegate = MockWKUIDelegate(); + mockScrollView = MockUIScrollView(); + mockWebsiteDataStore = MockWKWebsiteDataStore(); mockNavigationDelegate = MockWKNavigationDelegate(); mockWebViewWidgetProxy = MockWebViewWidgetProxy(); @@ -58,6 +70,13 @@ void main() { when(mockWebViewConfiguration.userContentController).thenReturn( mockUserContentController, ); + when(mockWebViewConfiguration.preferences).thenReturn(mockPreferences); + + when(mockWebView.scrollView).thenReturn(mockScrollView); + + when(mockWebViewConfiguration.websiteDataStore).thenReturn( + mockWebsiteDataStore, + ); mockCallbacksHandler = MockWebViewPlatformCallbacksHandler(); mockJavascriptChannelRegistry = MockJavascriptChannelRegistry(); @@ -99,7 +118,7 @@ void main() { await buildWidget(tester); final dynamic onCreateWebView = - verify(mockUIDelegate.onCreateWebView = captureAny).captured.single + verify(mockUIDelegate.setOnCreateWebView(captureAny)).captured.single as void Function(WKWebViewConfiguration, WKNavigationAction); const NSUrlRequest request = NSUrlRequest(url: 'https://google.com'); @@ -114,7 +133,56 @@ void main() { verify(mockWebView.loadRequest(request)); }); - group('$CreationParams', () { + group('CreationParams', () { + testWidgets('initialUrl', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + initialUrl: 'https://www.google.com', + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: false, + ), + ), + ); + final NSUrlRequest request = verify(mockWebView.loadRequest(captureAny)) + .captured + .single as NSUrlRequest; + expect(request.url, 'https://www.google.com'); + }); + + testWidgets('backgroundColor', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + backgroundColor: Colors.red, + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: false, + ), + ), + ); + + verify(mockWebView.setOpaque(false)); + verify(mockWebView.setBackgroundColor(Colors.transparent)); + verify(mockScrollView.setBackgroundColor(Colors.red)); + }); + + testWidgets('userAgent', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + userAgent: 'MyUserAgent', + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: false, + ), + ), + ); + + verify(mockWebView.setCustomUserAgent('MyUserAgent')); + }); + testWidgets('autoMediaPlaybackPolicy true', (WidgetTester tester) async { await buildWidget( tester, @@ -128,11 +196,11 @@ void main() { ), ); - verify( - mockWebViewConfiguration.mediaTypesRequiringUserActionForPlayback = - { + verify(mockWebViewConfiguration + .setMediaTypesRequiringUserActionForPlayback(< + WKAudiovisualMediaType>{ WKAudiovisualMediaType.all, - }); + })); }); testWidgets('autoMediaPlaybackPolicy false', (WidgetTester tester) async { @@ -147,11 +215,11 @@ void main() { ), ); - verify( - mockWebViewConfiguration.mediaTypesRequiringUserActionForPlayback = - { + verify(mockWebViewConfiguration + .setMediaTypesRequiringUserActionForPlayback(< + WKAudiovisualMediaType>{ WKAudiovisualMediaType.none, - }); + })); }); testWidgets('javascriptChannelNames', (WidgetTester tester) async { @@ -188,7 +256,159 @@ void main() { expect(javaScriptChannels[3], 'b'); }); - group('$WebSettings', () { + group('WebSettings', () { + testWidgets('javascriptMode', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + javascriptMode: JavascriptMode.unrestricted, + hasNavigationDelegate: false, + ), + ), + ); + + verify(mockPreferences.setJavaScriptEnabled(true)); + }); + + testWidgets('hasNavigationDelegate', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: true, + ), + ), + ); + + verify(mockNavigationDelegate + .setDecidePolicyForNavigationAction(argThat(isNotNull))); + }); + + testWidgets('userAgent', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.of('myUserAgent'), + hasNavigationDelegate: false, + ), + ), + ); + + verify(mockWebView.setCustomUserAgent('myUserAgent')); + }); + + testWidgets( + 'enabling zoom re-adds JavaScript channels', + (WidgetTester tester) async { + when(mockWebViewWidgetProxy.createScriptMessageHandler()) + .thenReturn( + MockWKScriptMessageHandler(), + ); + + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + zoomEnabled: false, + hasNavigationDelegate: false, + ), + javascriptChannelNames: {'myChannel'}, + ), + ); + + clearInteractions(mockUserContentController); + + await testController.updateSettings(WebSettings( + userAgent: const WebSetting.absent(), + zoomEnabled: true, + )); + + final List javaScriptChannels = verifyInOrder([ + mockUserContentController.removeAllUserScripts(), + mockUserContentController.removeAllScriptMessageHandlers(), + mockUserContentController.addScriptMessageHandler( + captureAny, + captureAny, + ), + ]).captured[2]; + + expect( + javaScriptChannels[0], + isA(), + ); + expect(javaScriptChannels[1], 'myChannel'); + }, + ); + + testWidgets( + 'enabling zoom removes script', + (WidgetTester tester) async { + when(mockWebViewWidgetProxy.createScriptMessageHandler()) + .thenReturn( + MockWKScriptMessageHandler(), + ); + + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + zoomEnabled: false, + hasNavigationDelegate: false, + ), + ), + ); + + clearInteractions(mockUserContentController); + + await testController.updateSettings(WebSettings( + userAgent: const WebSetting.absent(), + zoomEnabled: true, + )); + + verify(mockUserContentController.removeAllUserScripts()); + verify(mockUserContentController.removeAllScriptMessageHandlers()); + verifyNever(mockUserContentController.addScriptMessageHandler( + any, + any, + )); + }, + ); + + testWidgets('zoomEnabled is false', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + zoomEnabled: false, + hasNavigationDelegate: false, + ), + ), + ); + + final WKUserScript zoomScript = + verify(mockUserContentController.addUserScript(captureAny)) + .captured + .first as WKUserScript; + expect(zoomScript.isMainFrameOnly, isTrue); + expect(zoomScript.injectionTime, + WKUserScriptInjectionTime.atDocumentEnd); + expect( + zoomScript.source, + "var meta = document.createElement('meta');" + "meta.name = 'viewport';" + "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0," + "user-scalable=no';" + "var head = document.getElementsByTagName('head')[0];head.appendChild(meta);", + ); + }); + testWidgets('allowsInlineMediaPlayback', (WidgetTester tester) async { await buildWidget( tester, @@ -200,12 +420,368 @@ void main() { ), ); - verify(mockWebViewConfiguration.allowsInlineMediaPlayback = true); + verify(mockWebViewConfiguration.setAllowsInlineMediaPlayback(true)); }); }); }); - group('$WebKitWebViewPlatformController', () { + group('WebKitWebViewPlatformController', () { + testWidgets('loadFile', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadFile('/path/to/file.html'); + verify(mockWebView.loadFileUrl( + '/path/to/file.html', + readAccessUrl: '/path/to', + )); + }); + + testWidgets('loadFlutterAsset', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadFlutterAsset('test_assets/index.html'); + verify(mockWebView.loadFlutterAsset('test_assets/index.html')); + }); + + testWidgets('loadHtmlString', (WidgetTester tester) async { + await buildWidget(tester); + + const String htmlString = 'Test data.'; + await testController.loadHtmlString(htmlString, baseUrl: 'baseUrl'); + + verify(mockWebView.loadHtmlString( + 'Test data.', + baseUrl: 'baseUrl', + )); + }); + + testWidgets('loadUrl', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadUrl( + 'https://www.google.com', + {'a': 'header'}, + ); + + final NSUrlRequest request = verify(mockWebView.loadRequest(captureAny)) + .captured + .single as NSUrlRequest; + expect(request.url, 'https://www.google.com'); + expect(request.allHttpHeaderFields, {'a': 'header'}); + }); + + group('loadRequest', () { + testWidgets('Throws ArgumentError for empty scheme', + (WidgetTester tester) async { + await buildWidget(tester); + + expect( + () async => await testController.loadRequest( + WebViewRequest( + uri: Uri.parse('www.google.com'), + method: WebViewRequestMethod.get, + ), + ), + throwsA(const TypeMatcher())); + }); + + testWidgets('GET without headers', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadRequest(WebViewRequest( + uri: Uri.parse('https://www.google.com'), + method: WebViewRequestMethod.get, + )); + + final NSUrlRequest request = + verify(mockWebView.loadRequest(captureAny)).captured.single + as NSUrlRequest; + expect(request.url, 'https://www.google.com'); + expect(request.allHttpHeaderFields, {}); + expect(request.httpMethod, 'get'); + }); + + testWidgets('GET with headers', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadRequest(WebViewRequest( + uri: Uri.parse('https://www.google.com'), + method: WebViewRequestMethod.get, + headers: {'a': 'header'}, + )); + + final NSUrlRequest request = + verify(mockWebView.loadRequest(captureAny)).captured.single + as NSUrlRequest; + expect(request.url, 'https://www.google.com'); + expect(request.allHttpHeaderFields, {'a': 'header'}); + expect(request.httpMethod, 'get'); + }); + + testWidgets('POST without body', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadRequest(WebViewRequest( + uri: Uri.parse('https://www.google.com'), + method: WebViewRequestMethod.post, + )); + + final NSUrlRequest request = + verify(mockWebView.loadRequest(captureAny)).captured.single + as NSUrlRequest; + expect(request.url, 'https://www.google.com'); + expect(request.httpMethod, 'post'); + }); + + testWidgets('POST with body', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadRequest(WebViewRequest( + uri: Uri.parse('https://www.google.com'), + method: WebViewRequestMethod.post, + body: Uint8List.fromList('Test Body'.codeUnits))); + + final NSUrlRequest request = + verify(mockWebView.loadRequest(captureAny)).captured.single + as NSUrlRequest; + expect(request.url, 'https://www.google.com'); + expect(request.httpMethod, 'post'); + expect( + request.httpBody, + Uint8List.fromList('Test Body'.codeUnits), + ); + }); + }); + + testWidgets('canGoBack', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.canGoBack()).thenAnswer( + (_) => Future.value(false), + ); + expect(testController.canGoBack(), completion(false)); + }); + + testWidgets('canGoForward', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.canGoForward()).thenAnswer( + (_) => Future.value(true), + ); + expect(testController.canGoForward(), completion(true)); + }); + + testWidgets('goBack', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.goBack(); + verify(mockWebView.goBack()); + }); + + testWidgets('goForward', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.goForward(); + verify(mockWebView.goForward()); + }); + + testWidgets('reload', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.reload(); + verify(mockWebView.reload()); + }); + + testWidgets('evaluateJavascript', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value('returnString'), + ); + expect( + testController.evaluateJavascript('runJavaScript'), + completion('returnString'), + ); + }); + + testWidgets('evaluateJavascript with null return value', + (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value(), + ); + // The legacy implementation of webview_flutter_wkwebview would convert + // objects to strings before returning them to Dart. This verifies null + // is represented the way it is in Objective-C. + expect( + testController.evaluateJavascript('runJavaScript'), + completion('(null)'), + ); + }); + + testWidgets('evaluateJavascript with list return value', + (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value([1, 'string', null]), + ); + // The legacy implementation of webview_flutter_wkwebview would convert + // objects to strings before returning them to Dart. This verifies list + // is represented the way it is in Objective-C. + expect( + testController.evaluateJavascript('runJavaScript'), + completion('(1,string,"")'), + ); + }); + + testWidgets('evaluateJavascript with map return value', + (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value({ + 1: 'string', + null: null, + }), + ); + // The legacy implementation of webview_flutter_wkwebview would convert + // objects to strings before returning them to Dart. This verifies map + // is represented the way it is in Objective-C. + expect( + testController.evaluateJavascript('runJavaScript'), + completion('{1 = string;"" = ""}'), + ); + }); + + testWidgets('evaluateJavascript throws exception', + (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')) + .thenThrow(Error()); + expect( + testController.evaluateJavascript('runJavaScript'), + throwsA(isA()), + ); + }); + + testWidgets('runJavascriptReturningResult', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value('returnString'), + ); + expect( + testController.runJavascriptReturningResult('runJavaScript'), + completion('returnString'), + ); + }); + + testWidgets( + 'runJavascriptReturningResult throws error on null return value', + (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value(null), + ); + expect( + () => testController.runJavascriptReturningResult('runJavaScript'), + throwsArgumentError, + ); + }); + + testWidgets('runJavascript', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value('returnString'), + ); + expect( + testController.runJavascript('runJavaScript'), + completes, + ); + }); + + testWidgets( + 'runJavascript ignores exception with unsupported javascript type', + (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')) + .thenThrow(PlatformException( + code: '', + details: const NSError( + code: WKErrorCode.javaScriptResultTypeIsUnsupported, + domain: '', + localizedDescription: '', + ), + )); + expect( + testController.runJavascript('runJavaScript'), + completes, + ); + }); + + testWidgets('getTitle', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.getTitle()) + .thenAnswer((_) => Future.value('Web Title')); + expect(testController.getTitle(), completion('Web Title')); + }); + + testWidgets('scrollTo', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.scrollTo(2, 4); + verify(mockScrollView.setContentOffset(const Point(2.0, 4.0))); + }); + + testWidgets('scrollBy', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.scrollBy(2, 4); + verify(mockScrollView.scrollBy(const Point(2.0, 4.0))); + }); + + testWidgets('getScrollX', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockScrollView.getContentOffset()).thenAnswer( + (_) => Future>.value(const Point(8.0, 16.0))); + expect(testController.getScrollX(), completion(8.0)); + }); + + testWidgets('getScrollY', (WidgetTester tester) async { + await buildWidget(tester); + + await buildWidget(tester); + + when(mockScrollView.getContentOffset()).thenAnswer( + (_) => Future>.value(const Point(8.0, 16.0))); + expect(testController.getScrollY(), completion(16.0)); + }); + + testWidgets('clearCache', (WidgetTester tester) async { + await buildWidget(tester); + when( + mockWebsiteDataStore.removeDataOfTypes( + { + WKWebsiteDataTypes.memoryCache, + WKWebsiteDataTypes.diskCache, + WKWebsiteDataTypes.offlineWebApplicationCache, + WKWebsiteDataTypes.localStroage, + }, + DateTime.fromMillisecondsSinceEpoch(0), + ), + ).thenAnswer((_) => Future.value(false)); + + expect(testController.clearCache(), completes); + }); + testWidgets('addJavascriptChannels', (WidgetTester tester) async { when(mockWebViewWidgetProxy.createScriptMessageHandler()).thenReturn( MockWKScriptMessageHandler(), @@ -283,15 +859,53 @@ void main() { ); expect(userScripts[0].isMainFrameOnly, false); }); + + testWidgets('removeJavascriptChannels with zoom disabled', + (WidgetTester tester) async { + when(mockWebViewWidgetProxy.createScriptMessageHandler()).thenReturn( + MockWKScriptMessageHandler(), + ); + + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + zoomEnabled: false, + hasNavigationDelegate: false, + ), + ), + ); + + await testController.addJavascriptChannels({'c'}); + clearInteractions(mockUserContentController); + await testController.removeJavascriptChannels({'c'}); + + final WKUserScript zoomScript = + verify(mockUserContentController.addUserScript(captureAny)) + .captured + .first as WKUserScript; + expect(zoomScript.isMainFrameOnly, isTrue); + expect( + zoomScript.injectionTime, WKUserScriptInjectionTime.atDocumentEnd); + expect( + zoomScript.source, + "var meta = document.createElement('meta');" + "meta.name = 'viewport';" + "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0," + "user-scalable=no';" + "var head = document.getElementsByTagName('head')[0];head.appendChild(meta);", + ); + }); }); - group('$WebViewPlatformCallbacksHandler', () { + group('WebViewPlatformCallbacksHandler', () { testWidgets('onPageStarted', (WidgetTester tester) async { await buildWidget(tester); final dynamic didStartProvisionalNavigation = verify( - mockNavigationDelegate.didStartProvisionalNavigation = - captureAny) + mockNavigationDelegate + .setDidStartProvisionalNavigation(captureAny)) .captured .single as void Function(WKWebView, String); didStartProvisionalNavigation(mockWebView, 'https://google.com'); @@ -303,7 +917,7 @@ void main() { await buildWidget(tester); final dynamic didFinishNavigation = - verify(mockNavigationDelegate.didFinishNavigation = captureAny) + verify(mockNavigationDelegate.setDidFinishNavigation(captureAny)) .captured .single as void Function(WKWebView, String); didFinishNavigation(mockWebView, 'https://google.com'); @@ -316,7 +930,7 @@ void main() { await buildWidget(tester); final dynamic didFailNavigation = - verify(mockNavigationDelegate.didFailNavigation = captureAny) + verify(mockNavigationDelegate.setDidFailNavigation(captureAny)) .captured .single as void Function(WKWebView, NSError); @@ -344,8 +958,8 @@ void main() { await buildWidget(tester); final dynamic didFailProvisionalNavigation = verify( - mockNavigationDelegate.didFailProvisionalNavigation = - captureAny) + mockNavigationDelegate + .setDidFailProvisionalNavigation(captureAny)) .captured .single as void Function(WKWebView, NSError); @@ -377,8 +991,8 @@ void main() { await buildWidget(tester); final dynamic webViewWebContentProcessDidTerminate = verify( - mockNavigationDelegate.webViewWebContentProcessDidTerminate = - captureAny) + mockNavigationDelegate + .setWebViewWebContentProcessDidTerminate(captureAny)) .captured .single as void Function(WKWebView); webViewWebContentProcessDidTerminate(mockWebView); @@ -401,8 +1015,8 @@ void main() { await buildWidget(tester, hasNavigationDelegate: true); final dynamic decidePolicyForNavigationAction = verify( - mockNavigationDelegate.decidePolicyForNavigationAction = - captureAny) + mockNavigationDelegate + .setDecidePolicyForNavigationAction(captureAny)) .captured .single as Future Function( @@ -429,9 +1043,36 @@ void main() { isForMainFrame: false, )); }); + + testWidgets('onProgress', (WidgetTester tester) async { + await buildWidget(tester, hasProgressTracking: true); + final dynamic observeValue = + verify(mockWebView.setObserveValue(captureAny)).captured.single + as void Function( + String keyPath, + NSObject object, + Map change, + ); + + verify(mockWebView.addObserver( + mockWebView, + keyPath: 'estimatedProgress', + options: { + NSKeyValueObservingOptions.newValue, + }, + )); + + observeValue( + 'estimatedProgress', + mockWebView, + {NSKeyValueChangeKey.newValue: 0.32}, + ); + + verify(mockCallbacksHandler.onProgress(32)); + }); }); - group('$JavascriptChannelRegistry', () { + group('JavascriptChannelRegistry', () { testWidgets('onJavascriptChannelMessage', (WidgetTester tester) async { when(mockWebViewWidgetProxy.createScriptMessageHandler()).thenReturn( MockWKScriptMessageHandler(), @@ -447,7 +1088,7 @@ void main() { .single as MockWKScriptMessageHandler; final dynamic didReceiveScriptMessage = - verify(messageHandler.didReceiveScriptMessage = captureAny) + verify(messageHandler.setDidReceiveScriptMessage(captureAny)) .captured .single as void Function( WKUserContentController userContentController, diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.mocks.dart index 9579b23c84a0..1138d13f0c25 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.mocks.dart @@ -2,19 +2,23 @@ // in webview_flutter_wkwebview/example/ios/.symlinks/plugins/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.dart. // Do not manually edit this file. -import 'dart:async' as _i3; +import 'dart:async' as _i5; +import 'dart:math' as _i2; +import 'dart:ui' as _i6; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_flutter_platform_interface/src/types/javascript_channel.dart' - as _i6; -import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i7; + as _i9; +import 'package:webview_flutter_platform_interface/src/types/types.dart' + as _i10; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' - as _i5; + as _i8; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart' - as _i4; -import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i2; + as _i7; +import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart' as _i4; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i3; import 'package:webview_flutter_wkwebview/src/web_kit_webview_widget.dart' - as _i8; + as _i11; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -26,233 +30,493 @@ import 'package:webview_flutter_wkwebview/src/web_kit_webview_widget.dart' // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types -class _FakeWKWebViewConfiguration_0 extends _i1.Fake - implements _i2.WKWebViewConfiguration {} +class _FakePoint_0 extends _i1.Fake implements _i2.Point {} + +class _FakeWKWebViewConfiguration_1 extends _i1.Fake + implements _i3.WKWebViewConfiguration {} + +class _FakeUIScrollView_2 extends _i1.Fake implements _i4.UIScrollView {} + +class _FakeWKUserContentController_3 extends _i1.Fake + implements _i3.WKUserContentController {} -class _FakeWKUserContentController_1 extends _i1.Fake - implements _i2.WKUserContentController {} +class _FakeWKPreferences_4 extends _i1.Fake implements _i3.WKPreferences {} -class _FakeWKWebView_2 extends _i1.Fake implements _i2.WKWebView {} +class _FakeWKWebsiteDataStore_5 extends _i1.Fake + implements _i3.WKWebsiteDataStore {} -class _FakeWKScriptMessageHandler_3 extends _i1.Fake - implements _i2.WKScriptMessageHandler {} +class _FakeWKHttpCookieStore_6 extends _i1.Fake + implements _i3.WKHttpCookieStore {} -class _FakeWKUIDelegate_4 extends _i1.Fake implements _i2.WKUIDelegate {} +class _FakeWKWebView_7 extends _i1.Fake implements _i3.WKWebView {} -class _FakeWKNavigationDelegate_5 extends _i1.Fake - implements _i2.WKNavigationDelegate {} +class _FakeWKScriptMessageHandler_8 extends _i1.Fake + implements _i3.WKScriptMessageHandler {} + +class _FakeWKUIDelegate_9 extends _i1.Fake implements _i3.WKUIDelegate {} + +class _FakeWKNavigationDelegate_10 extends _i1.Fake + implements _i3.WKNavigationDelegate {} + +/// A class which mocks [UIScrollView]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUIScrollView extends _i1.Mock implements _i4.UIScrollView { + MockUIScrollView() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future<_i2.Point> getContentOffset() => (super.noSuchMethod( + Invocation.method(#getContentOffset, []), + returnValue: Future<_i2.Point>.value(_FakePoint_0())) + as _i5.Future<_i2.Point>); + @override + _i5.Future scrollBy(_i2.Point? offset) => + (super.noSuchMethod(Invocation.method(#scrollBy, [offset]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setContentOffset(_i2.Point? offset) => + (super.noSuchMethod(Invocation.method(#setContentOffset, [offset]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setBackgroundColor(_i6.Color? color) => + (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setOpaque(bool? opaque) => + (super.noSuchMethod(Invocation.method(#setOpaque, [opaque]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future dispose() => + (super.noSuchMethod(Invocation.method(#dispose, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setObserveValue( + void Function( + String, _i7.NSObject, Map<_i7.NSKeyValueChangeKey, Object?>)? + observeValue) => + (super.noSuchMethod(Invocation.method(#setObserveValue, [observeValue]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); +} /// A class which mocks [WKNavigationDelegate]. /// /// See the documentation for Mockito's code generation for more information. class MockWKNavigationDelegate extends _i1.Mock - implements _i2.WKNavigationDelegate { + implements _i3.WKNavigationDelegate { MockWKNavigationDelegate() { _i1.throwOnMissingStub(this); } @override - set didStartProvisionalNavigation( - void Function(_i2.WKWebView, String?)? + _i5.Future setDidStartProvisionalNavigation( + void Function(_i3.WKWebView, String?)? didStartProvisionalNavigation) => - super.noSuchMethod( - Invocation.setter( - #didStartProvisionalNavigation, didStartProvisionalNavigation), - returnValueForMissingStub: null); + (super.noSuchMethod( + Invocation.method(#setDidStartProvisionalNavigation, + [didStartProvisionalNavigation]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); @override - set didFinishNavigation( - void Function(_i2.WKWebView, String?)? didFinishNavigation) => - super.noSuchMethod( - Invocation.setter(#didFinishNavigation, didFinishNavigation), - returnValueForMissingStub: null); + _i5.Future setDidFinishNavigation( + void Function(_i3.WKWebView, String?)? didFinishNavigation) => + (super.noSuchMethod( + Invocation.method(#setDidFinishNavigation, [didFinishNavigation]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); @override - set decidePolicyForNavigationAction( - _i3.Future<_i2.WKNavigationActionPolicy> Function( - _i2.WKWebView, _i2.WKNavigationAction)? + _i5.Future setDecidePolicyForNavigationAction( + _i5.Future<_i3.WKNavigationActionPolicy> Function( + _i3.WKWebView, _i3.WKNavigationAction)? decidePolicyForNavigationAction) => - super.noSuchMethod( - Invocation.setter(#decidePolicyForNavigationAction, - decidePolicyForNavigationAction), - returnValueForMissingStub: null); + (super.noSuchMethod( + Invocation.method(#setDecidePolicyForNavigationAction, + [decidePolicyForNavigationAction]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); @override - set didFailNavigation( - void Function(_i2.WKWebView, _i4.NSError)? didFailNavigation) => - super.noSuchMethod( - Invocation.setter(#didFailNavigation, didFailNavigation), - returnValueForMissingStub: null); + _i5.Future setDidFailNavigation( + void Function(_i3.WKWebView, _i7.NSError)? didFailNavigation) => + (super.noSuchMethod( + Invocation.method(#setDidFailNavigation, [didFailNavigation]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); @override - set didFailProvisionalNavigation( - void Function(_i2.WKWebView, _i4.NSError)? + _i5.Future setDidFailProvisionalNavigation( + void Function(_i3.WKWebView, _i7.NSError)? didFailProvisionalNavigation) => - super.noSuchMethod( - Invocation.setter( - #didFailProvisionalNavigation, didFailProvisionalNavigation), - returnValueForMissingStub: null); + (super.noSuchMethod( + Invocation.method( + #setDidFailProvisionalNavigation, [didFailProvisionalNavigation]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); @override - set webViewWebContentProcessDidTerminate( - void Function(_i2.WKWebView)? webViewWebContentProcessDidTerminate) => - super.noSuchMethod( - Invocation.setter(#webViewWebContentProcessDidTerminate, - webViewWebContentProcessDidTerminate), - returnValueForMissingStub: null); + _i5.Future setWebViewWebContentProcessDidTerminate( + void Function(_i3.WKWebView)? webViewWebContentProcessDidTerminate) => + (super.noSuchMethod( + Invocation.method(#setWebViewWebContentProcessDidTerminate, + [webViewWebContentProcessDidTerminate]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future dispose() => + (super.noSuchMethod(Invocation.method(#dispose, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setObserveValue( + void Function( + String, _i7.NSObject, Map<_i7.NSKeyValueChangeKey, Object?>)? + observeValue) => + (super.noSuchMethod(Invocation.method(#setObserveValue, [observeValue]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); +} + +/// A class which mocks [WKPreferences]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWKPreferences extends _i1.Mock implements _i3.WKPreferences { + MockWKPreferences() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future setJavaScriptEnabled(bool? enabled) => + (super.noSuchMethod(Invocation.method(#setJavaScriptEnabled, [enabled]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); } /// A class which mocks [WKScriptMessageHandler]. /// /// See the documentation for Mockito's code generation for more information. class MockWKScriptMessageHandler extends _i1.Mock - implements _i2.WKScriptMessageHandler { + implements _i3.WKScriptMessageHandler { MockWKScriptMessageHandler() { _i1.throwOnMissingStub(this); } @override - set didReceiveScriptMessage( - void Function(_i2.WKUserContentController, _i2.WKScriptMessage)? + _i5.Future setDidReceiveScriptMessage( + void Function(_i3.WKUserContentController, _i3.WKScriptMessage)? didReceiveScriptMessage) => - super.noSuchMethod( - Invocation.setter(#didReceiveScriptMessage, didReceiveScriptMessage), - returnValueForMissingStub: null); + (super.noSuchMethod( + Invocation.method( + #setDidReceiveScriptMessage, [didReceiveScriptMessage]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); } /// A class which mocks [WKWebView]. /// /// See the documentation for Mockito's code generation for more information. -class MockWKWebView extends _i1.Mock implements _i2.WKWebView { +class MockWKWebView extends _i1.Mock implements _i3.WKWebView { MockWKWebView() { _i1.throwOnMissingStub(this); } @override - _i2.WKWebViewConfiguration get configuration => + _i3.WKWebViewConfiguration get configuration => (super.noSuchMethod(Invocation.getter(#configuration), - returnValue: _FakeWKWebViewConfiguration_0()) - as _i2.WKWebViewConfiguration); + returnValue: _FakeWKWebViewConfiguration_1()) + as _i3.WKWebViewConfiguration); @override - set uiDelegate(_i2.WKUIDelegate? delegate) => - super.noSuchMethod(Invocation.setter(#uiDelegate, delegate), - returnValueForMissingStub: null); + _i4.UIScrollView get scrollView => + (super.noSuchMethod(Invocation.getter(#scrollView), + returnValue: _FakeUIScrollView_2()) as _i4.UIScrollView); @override - set navigationDelegate(_i2.WKNavigationDelegate? delegate) => - super.noSuchMethod(Invocation.setter(#navigationDelegate, delegate), - returnValueForMissingStub: null); + _i5.Future setUIDelegate(_i3.WKUIDelegate? delegate) => + (super.noSuchMethod(Invocation.method(#setUIDelegate, [delegate]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setNavigationDelegate(_i3.WKNavigationDelegate? delegate) => + (super.noSuchMethod(Invocation.method(#setNavigationDelegate, [delegate]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i3.Future get url => (super.noSuchMethod(Invocation.getter(#url), - returnValue: Future.value()) as _i3.Future); + _i5.Future getUrl() => + (super.noSuchMethod(Invocation.method(#getUrl, []), + returnValue: Future.value()) as _i5.Future); @override - _i3.Future loadRequest(_i4.NSUrlRequest? request) => + _i5.Future getEstimatedProgress() => + (super.noSuchMethod(Invocation.method(#getEstimatedProgress, []), + returnValue: Future.value(0.0)) as _i5.Future); + @override + _i5.Future loadRequest(_i7.NSUrlRequest? request) => (super.noSuchMethod(Invocation.method(#loadRequest, [request]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future loadHtmlString(String? string, {String? baseUrl}) => + (super.noSuchMethod( + Invocation.method(#loadHtmlString, [string], {#baseUrl: baseUrl}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future loadFileUrl(String? url, {String? readAccessUrl}) => + (super.noSuchMethod( + Invocation.method( + #loadFileUrl, [url], {#readAccessUrl: readAccessUrl}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future loadFlutterAsset(String? key) => + (super.noSuchMethod(Invocation.method(#loadFlutterAsset, [key]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future canGoBack() => + (super.noSuchMethod(Invocation.method(#canGoBack, []), + returnValue: Future.value(false)) as _i5.Future); + @override + _i5.Future canGoForward() => + (super.noSuchMethod(Invocation.method(#canGoForward, []), + returnValue: Future.value(false)) as _i5.Future); + @override + _i5.Future goBack() => + (super.noSuchMethod(Invocation.method(#goBack, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future goForward() => + (super.noSuchMethod(Invocation.method(#goForward, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future reload() => + (super.noSuchMethod(Invocation.method(#reload, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future getTitle() => + (super.noSuchMethod(Invocation.method(#getTitle, []), + returnValue: Future.value()) as _i5.Future); + @override + _i5.Future setAllowsBackForwardNavigationGestures(bool? allow) => + (super.noSuchMethod( + Invocation.method(#setAllowsBackForwardNavigationGestures, [allow]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setCustomUserAgent(String? userAgent) => + (super.noSuchMethod(Invocation.method(#setCustomUserAgent, [userAgent]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future evaluateJavaScript(String? javaScriptString) => (super + .noSuchMethod(Invocation.method(#evaluateJavaScript, [javaScriptString]), + returnValue: Future.value()) as _i5.Future); + @override + _i5.Future setBackgroundColor(_i6.Color? color) => + (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setOpaque(bool? opaque) => + (super.noSuchMethod(Invocation.method(#setOpaque, [opaque]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future dispose() => + (super.noSuchMethod(Invocation.method(#dispose, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setObserveValue( + void Function( + String, _i7.NSObject, Map<_i7.NSKeyValueChangeKey, Object?>)? + observeValue) => + (super.noSuchMethod(Invocation.method(#setObserveValue, [observeValue]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); } /// A class which mocks [WKWebViewConfiguration]. /// /// See the documentation for Mockito's code generation for more information. class MockWKWebViewConfiguration extends _i1.Mock - implements _i2.WKWebViewConfiguration { + implements _i3.WKWebViewConfiguration { MockWKWebViewConfiguration() { _i1.throwOnMissingStub(this); } @override - _i2.WKUserContentController get userContentController => + _i3.WKUserContentController get userContentController => (super.noSuchMethod(Invocation.getter(#userContentController), - returnValue: _FakeWKUserContentController_1()) - as _i2.WKUserContentController); + returnValue: _FakeWKUserContentController_3()) + as _i3.WKUserContentController); @override - set userContentController( - _i2.WKUserContentController? _userContentController) => - super.noSuchMethod( - Invocation.setter(#userContentController, _userContentController), - returnValueForMissingStub: null); + _i3.WKPreferences get preferences => + (super.noSuchMethod(Invocation.getter(#preferences), + returnValue: _FakeWKPreferences_4()) as _i3.WKPreferences); @override - set allowsInlineMediaPlayback(bool? allow) => - super.noSuchMethod(Invocation.setter(#allowsInlineMediaPlayback, allow), - returnValueForMissingStub: null); + _i3.WKWebsiteDataStore get websiteDataStore => + (super.noSuchMethod(Invocation.getter(#websiteDataStore), + returnValue: _FakeWKWebsiteDataStore_5()) as _i3.WKWebsiteDataStore); @override - set mediaTypesRequiringUserActionForPlayback( - Set<_i2.WKAudiovisualMediaType>? types) => - super.noSuchMethod( - Invocation.setter(#mediaTypesRequiringUserActionForPlayback, types), - returnValueForMissingStub: null); + _i5.Future setAllowsInlineMediaPlayback(bool? allow) => (super + .noSuchMethod(Invocation.method(#setAllowsInlineMediaPlayback, [allow]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setMediaTypesRequiringUserActionForPlayback( + Set<_i3.WKAudiovisualMediaType>? types) => + (super.noSuchMethod( + Invocation.method( + #setMediaTypesRequiringUserActionForPlayback, [types]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); +} + +/// A class which mocks [WKWebsiteDataStore]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWKWebsiteDataStore extends _i1.Mock + implements _i3.WKWebsiteDataStore { + MockWKWebsiteDataStore() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.WKHttpCookieStore get httpCookieStore => + (super.noSuchMethod(Invocation.getter(#httpCookieStore), + returnValue: _FakeWKHttpCookieStore_6()) as _i3.WKHttpCookieStore); + @override + _i5.Future removeDataOfTypes( + Set<_i3.WKWebsiteDataTypes>? dataTypes, DateTime? since) => + (super.noSuchMethod( + Invocation.method(#removeDataOfTypes, [dataTypes, since]), + returnValue: Future.value(false)) as _i5.Future); } /// A class which mocks [WKUIDelegate]. /// /// See the documentation for Mockito's code generation for more information. -class MockWKUIDelegate extends _i1.Mock implements _i2.WKUIDelegate { +class MockWKUIDelegate extends _i1.Mock implements _i3.WKUIDelegate { MockWKUIDelegate() { _i1.throwOnMissingStub(this); } @override - set onCreateWebView( - void Function(_i2.WKWebViewConfiguration, _i2.WKNavigationAction)? - onCreateeWebView) => - super.noSuchMethod(Invocation.setter(#onCreateWebView, onCreateeWebView), - returnValueForMissingStub: null); + _i5.Future setOnCreateWebView( + void Function(_i3.WKWebViewConfiguration, _i3.WKNavigationAction)? + onCreateWebView) => + (super.noSuchMethod( + Invocation.method(#setOnCreateWebView, [onCreateWebView]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); } /// A class which mocks [WKUserContentController]. /// /// See the documentation for Mockito's code generation for more information. class MockWKUserContentController extends _i1.Mock - implements _i2.WKUserContentController { + implements _i3.WKUserContentController { MockWKUserContentController() { _i1.throwOnMissingStub(this); } @override - _i3.Future addScriptMessageHandler( - _i2.WKScriptMessageHandler? handler, String? name) => + _i5.Future addScriptMessageHandler( + _i3.WKScriptMessageHandler? handler, String? name) => (super.noSuchMethod( Invocation.method(#addScriptMessageHandler, [handler, name]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i3.Future removeScriptMessageHandler(String? name) => (super + _i5.Future removeScriptMessageHandler(String? name) => (super .noSuchMethod(Invocation.method(#removeScriptMessageHandler, [name]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i3.Future removeAllScriptMessageHandlers() => (super.noSuchMethod( + _i5.Future removeAllScriptMessageHandlers() => (super.noSuchMethod( Invocation.method(#removeAllScriptMessageHandlers, []), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i3.Future addUserScript(_i2.WKUserScript? userScript) => + _i5.Future addUserScript(_i3.WKUserScript? userScript) => (super.noSuchMethod(Invocation.method(#addUserScript, [userScript]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i3.Future removeAllUserScripts() => + _i5.Future removeAllUserScripts() => (super.noSuchMethod(Invocation.method(#removeAllUserScripts, []), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); } /// A class which mocks [JavascriptChannelRegistry]. /// /// See the documentation for Mockito's code generation for more information. class MockJavascriptChannelRegistry extends _i1.Mock - implements _i5.JavascriptChannelRegistry { + implements _i8.JavascriptChannelRegistry { MockJavascriptChannelRegistry() { _i1.throwOnMissingStub(this); } @override - Map get channels => + Map get channels => (super.noSuchMethod(Invocation.getter(#channels), - returnValue: {}) - as Map); + returnValue: {}) + as Map); @override void onJavascriptChannelMessage(String? channel, String? message) => super.noSuchMethod( Invocation.method(#onJavascriptChannelMessage, [channel, message]), returnValueForMissingStub: null); @override - void updateJavascriptChannelsFromSet(Set<_i6.JavascriptChannel>? channels) => + void updateJavascriptChannelsFromSet(Set<_i9.JavascriptChannel>? channels) => super.noSuchMethod( Invocation.method(#updateJavascriptChannelsFromSet, [channels]), returnValueForMissingStub: null); @@ -262,17 +526,17 @@ class MockJavascriptChannelRegistry extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockWebViewPlatformCallbacksHandler extends _i1.Mock - implements _i5.WebViewPlatformCallbacksHandler { + implements _i8.WebViewPlatformCallbacksHandler { MockWebViewPlatformCallbacksHandler() { _i1.throwOnMissingStub(this); } @override - _i3.FutureOr onNavigationRequest({String? url, bool? isForMainFrame}) => + _i5.FutureOr onNavigationRequest({String? url, bool? isForMainFrame}) => (super.noSuchMethod( Invocation.method(#onNavigationRequest, [], {#url: url, #isForMainFrame: isForMainFrame}), - returnValue: Future.value(false)) as _i3.FutureOr); + returnValue: Future.value(false)) as _i5.FutureOr); @override void onPageStarted(String? url) => super.noSuchMethod(Invocation.method(#onPageStarted, [url]), @@ -286,7 +550,7 @@ class MockWebViewPlatformCallbacksHandler extends _i1.Mock super.noSuchMethod(Invocation.method(#onProgress, [progress]), returnValueForMissingStub: null); @override - void onWebResourceError(_i7.WebResourceError? error) => + void onWebResourceError(_i10.WebResourceError? error) => super.noSuchMethod(Invocation.method(#onWebResourceError, [error]), returnValueForMissingStub: null); } @@ -295,26 +559,26 @@ class MockWebViewPlatformCallbacksHandler extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockWebViewWidgetProxy extends _i1.Mock - implements _i8.WebViewWidgetProxy { + implements _i11.WebViewWidgetProxy { MockWebViewWidgetProxy() { _i1.throwOnMissingStub(this); } @override - _i2.WKWebView createWebView(_i2.WKWebViewConfiguration? configuration) => + _i3.WKWebView createWebView(_i3.WKWebViewConfiguration? configuration) => (super.noSuchMethod(Invocation.method(#createWebView, [configuration]), - returnValue: _FakeWKWebView_2()) as _i2.WKWebView); + returnValue: _FakeWKWebView_7()) as _i3.WKWebView); @override - _i2.WKScriptMessageHandler createScriptMessageHandler() => + _i3.WKScriptMessageHandler createScriptMessageHandler() => (super.noSuchMethod(Invocation.method(#createScriptMessageHandler, []), - returnValue: _FakeWKScriptMessageHandler_3()) - as _i2.WKScriptMessageHandler); + returnValue: _FakeWKScriptMessageHandler_8()) + as _i3.WKScriptMessageHandler); @override - _i2.WKUIDelegate createUIDelgate() => + _i3.WKUIDelegate createUIDelgate() => (super.noSuchMethod(Invocation.method(#createUIDelgate, []), - returnValue: _FakeWKUIDelegate_4()) as _i2.WKUIDelegate); + returnValue: _FakeWKUIDelegate_9()) as _i3.WKUIDelegate); @override - _i2.WKNavigationDelegate createNavigationDelegate() => (super.noSuchMethod( + _i3.WKNavigationDelegate createNavigationDelegate() => (super.noSuchMethod( Invocation.method(#createNavigationDelegate, []), - returnValue: _FakeWKNavigationDelegate_5()) as _i2.WKNavigationDelegate); + returnValue: _FakeWKNavigationDelegate_10()) as _i3.WKNavigationDelegate); } diff --git a/script/configs/exclude_integration_win32.yaml b/script/configs/exclude_integration_win32.yaml index 4626fbd79ce7..09306691e5ed 100644 --- a/script/configs/exclude_integration_win32.yaml +++ b/script/configs/exclude_integration_win32.yaml @@ -1,3 +1,4 @@ # Can't use Flutter integration tests due to native modal UI. - file_selector - file_selector_windows +- image_picker_windows \ No newline at end of file diff --git a/script/configs/temp_exclude_excerpt.yaml b/script/configs/temp_exclude_excerpt.yaml new file mode 100644 index 000000000000..fc8454d75a0c --- /dev/null +++ b/script/configs/temp_exclude_excerpt.yaml @@ -0,0 +1,27 @@ +# Packages that have not yet adopted code-excerpt. +# +# This only exists to allow incrementally adopting the new requirement. +# Packages shoud never be added to this list. + +# TODO(ecosystem): Remove everything from this list. See +# https://github.com/flutter/flutter/issues/102679 +- camera_web +- espresso +- file_selector/file_selector +- google_maps_flutter/google_maps_flutter +- google_sign_in/google_sign_in +- google_sign_in_web +- image_picker/image_picker +- image_picker_for_web +- in_app_purchase/in_app_purchase +- ios_platform_images +- local_auth/local_auth +- path_provider/path_provider +- plugin_platform_interface +- quick_actions/quick_actions +- shared_preferences/shared_preferences +- url_launcher/url_launcher +- video_player/video_player +- webview_flutter/webview_flutter +- webview_flutter_android +- webview_flutter_web diff --git a/script/install_chromium.sh b/script/install_chromium.sh index 1cb38af05496..b7d787626d55 100755 --- a/script/install_chromium.sh +++ b/script/install_chromium.sh @@ -10,24 +10,35 @@ readonly TARGET_DIR=$1 # The build of Chromium used to test web functionality. # # Chromium builds can be located here: https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/ -readonly CHROMIUM_BUILD=768968 -# The ChromeDriver version corresponding to the build above. See -# https://chromedriver.chromium.org/downloads -# for versions mappings when updating Chromium. -readonly CHROME_DRIVER_VERSION=84.0.4147.30 +# +# Check: https://github.com/flutter/engine/blob/master/lib/web_ui/dev/browser_lock.yaml +readonly CHROMIUM_BUILD=929514 + +# The correct ChromeDriver is distributed alongside the chromium build above, as +# `chromedriver_linux64.zip`, so no need to hardcode any extra info about it. +readonly DOWNLOAD_ROOT="https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F${CHROMIUM_BUILD}%2F" # Install Chromium. mkdir "$TARGET_DIR" -wget --no-verbose "https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F${CHROMIUM_BUILD}%2Fchrome-linux.zip?alt=media" -O "$TARGET_DIR"/chromium.zip -unzip "$TARGET_DIR"/chromium.zip -d "$TARGET_DIR"/ +readonly CHROMIUM_ZIP_FILE="$TARGET_DIR/chromium.zip" +wget --no-verbose "${DOWNLOAD_ROOT}chrome-linux.zip?alt=media" -O "$CHROMIUM_ZIP_FILE" +unzip -q "$CHROMIUM_ZIP_FILE" -d "$TARGET_DIR/" # Install ChromeDriver. readonly DRIVER_ZIP_FILE="$TARGET_DIR/chromedriver.zip" -wget --no-verbose "https://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip" -O "$DRIVER_ZIP_FILE" -unzip "$DRIVER_ZIP_FILE" -d "$TARGET_DIR/chromedriver" +wget --no-verbose "${DOWNLOAD_ROOT}chromedriver_linux64.zip?alt=media" -O "$DRIVER_ZIP_FILE" +unzip -q "$DRIVER_ZIP_FILE" -d "$TARGET_DIR/" +# Rename TARGET_DIR/chromedriver_linux64 to the expected TARGET_DIR/chromedriver +mv -T "$TARGET_DIR/chromedriver_linux64" "$TARGET_DIR/chromedriver" + +export CHROME_EXECUTABLE="$TARGET_DIR/chrome-linux/chrome" # Echo info at the end for ease of debugging. -export CHROME_EXECUTABLE="$TARGET_DIR"/chrome-linux/chrome -echo $CHROME_EXECUTABLE -$CHROME_EXECUTABLE --version -echo "ChromeDriver $CHROME_DRIVER_VERSION" +set +x +echo +readonly CHROMEDRIVER_EXECUTABLE="$TARGET_DIR/chromedriver/chromedriver" +echo "$CHROME_EXECUTABLE" +"$CHROME_EXECUTABLE" --version +echo "$CHROMEDRIVER_EXECUTABLE" +"$CHROMEDRIVER_EXECUTABLE" --version +echo diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 7e950352facb..b2319c63dc48 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,37 @@ -## NEXT +## 0.8.5 +- Updates `test` to inculde the Dart unit tests of examples, if any. +- `drive-examples` now supports non-plugin packages. +- Commands that iterate over examples now include non-Flutter example packages. + +## 0.8.4 + +- `readme-check` now validates that there's a info tag on code blocks to + identify (and for supported languages, syntax highlight) the language. +- `readme-check` now has a `--require-excerpts` flag to require that any Dart + code blocks be managed by `code_excerpter`. + +## 0.8.3 + +- Adds a new `update-excerpts` command to maintain README files using the + `code-excerpter` package from flutter/site-shared. +- `license-check` now ignores submodules. +- Allows `make-deps-path-based` to skip packages it has alredy rewritten, so + that running multiple times won't fail after the first time. +- Removes UWP support, since Flutter has dropped support for UWP. + +## 0.8.2+1 + +- Adds a new `readme-check` command. +- Updates `publish-plugin` command documentation. +- Fixes `all-plugins-app` to preserve the original application's Dart SDK + version to avoid changing language feature opt-ins that the template may + rely on. +- Fixes `custom-test` to run `pub get` before running Dart test scripts. + +## 0.8.2 + +- Adds a new `custom-test` command. - Switches from deprecated `flutter packages` alias to `flutter pub`. ## 0.8.1 diff --git a/script/tool/README.md b/script/tool/README.md index 265d3868fc37..d52ee08fdc34 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -107,13 +107,31 @@ dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --ios --android dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --macos --packages plugin_name ``` +### Update README.md from Example Sources + +`update-excerpts` requires sources that are in a submodule. If you didn't clone +with submodules, you will need to `git submodule update --init --recursive` +before running this command. + +```sh +cd +dart run ./script/tool/bin/flutter_plugin_tools.dart update-excerpts --packages plugin_name +``` + ### Publish a Release -``sh +**Releases are automated for `flutter/plugins` and `flutter/packages`.** + +The manual procedure described here is _deprecated_, and should only be used when +the automated process fails. Please, read +[Releasing a Plugin or Package](https://github.com/flutter/flutter/wiki/Releasing-a-Plugin-or-Package) +on the Flutter Wiki first. + +```sh cd git checkout -dart run ./script/tool/bin/flutter_plugin_tools.dart publish-plugin --package -`` +dart run ./script/tool/bin/flutter_plugin_tools.dart publish-plugin --packages +``` By default the tool tries to push tags to the `upstream` remote, but some additional settings can be configured. Run `dart run ./script/tool/bin/flutter_plugin_tools.dart @@ -127,10 +145,6 @@ _everything_, including untracked or uncommitted files in version control. directory and refuse to publish if there are any mismatched files with version control present. -Automated publishing is under development. Follow -[flutter/flutter#27258](https://github.com/flutter/flutter/issues/27258) -for updates. - ## Updating the Tool For flutter/plugins, just changing the source here is all that's needed. diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index b88cfe309258..1aade3575559 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -37,8 +37,7 @@ const String _flutterBuildTypeIOS = 'ios'; const String _flutterBuildTypeLinux = 'linux'; const String _flutterBuildTypeMacOS = 'macos'; const String _flutterBuildTypeWeb = 'web'; -const String _flutterBuildTypeWin32 = 'windows'; -const String _flutterBuildTypeWinUwp = 'winuwp'; +const String _flutterBuildTypeWindows = 'windows'; /// A command to build the example applications for packages. class BuildExamplesCommand extends PackageLoopingCommand { @@ -52,7 +51,6 @@ class BuildExamplesCommand extends PackageLoopingCommand { argParser.addFlag(platformMacOS); argParser.addFlag(platformWeb); argParser.addFlag(platformWindows); - argParser.addFlag(platformWinUwp); argParser.addFlag(platformIOS); argParser.addFlag(_platformFlagApk); argParser.addOption( @@ -93,16 +91,9 @@ class BuildExamplesCommand extends PackageLoopingCommand { flutterBuildType: _flutterBuildTypeWeb, ), platformWindows: const _PlatformDetails( - 'Win32', + 'Windows', pluginPlatform: platformWindows, - pluginPlatformVariant: platformVariantWin32, - flutterBuildType: _flutterBuildTypeWin32, - ), - platformWinUwp: const _PlatformDetails( - 'UWP', - pluginPlatform: platformWindows, - pluginPlatformVariant: platformVariantWinUwp, - flutterBuildType: _flutterBuildTypeWinUwp, + flutterBuildType: _flutterBuildTypeWindows, ), }; @@ -146,9 +137,8 @@ class BuildExamplesCommand extends PackageLoopingCommand { // no package-level platform information for non-plugin packages. final Set<_PlatformDetails> buildPlatforms = isPlugin ? requestedPlatforms - .where((_PlatformDetails platform) => pluginSupportsPlatform( - platform.pluginPlatform, package, - variant: platform.pluginPlatformVariant)) + .where((_PlatformDetails platform) => + pluginSupportsPlatform(platform.pluginPlatform, package)) .toSet() : requestedPlatforms.toSet(); @@ -280,22 +270,6 @@ class BuildExamplesCommand extends PackageLoopingCommand { }) async { final String enableExperiment = getStringArg(kEnableExperiment); - // The UWP template is not yet stable, so the UWP directory - // needs to be created on the fly with 'flutter create .' - Directory? temporaryPlatformDirectory; - if (flutterBuildType == _flutterBuildTypeWinUwp) { - final Directory uwpDirectory = example.directory.childDirectory('winuwp'); - if (!uwpDirectory.existsSync()) { - print('Creating temporary winuwp folder'); - final int exitCode = await processRunner.runAndStream(flutterCommand, - ['create', '--platforms=$platformWinUwp', '.'], - workingDir: example.directory); - if (exitCode == 0) { - temporaryPlatformDirectory = uwpDirectory; - } - } - } - final int exitCode = await processRunner.runAndStream( flutterCommand, [ @@ -308,13 +282,6 @@ class BuildExamplesCommand extends PackageLoopingCommand { ], workingDir: example.directory, ); - - if (temporaryPlatformDirectory != null && - temporaryPlatformDirectory.existsSync()) { - print('Cleaning up ${temporaryPlatformDirectory.path}'); - temporaryPlatformDirectory.deleteSync(recursive: true); - } - return exitCode == 0; } } @@ -324,7 +291,6 @@ class _PlatformDetails { const _PlatformDetails( this.label, { required this.pluginPlatform, - this.pluginPlatformVariant, required this.flutterBuildType, this.extraBuildFlags = const [], }); @@ -335,10 +301,6 @@ class _PlatformDetails { /// The key in a pubspec's platform: entry. final String pluginPlatform; - /// The supportedVariants key under a plugin's [pluginPlatform] entry, if - /// applicable. - final String? pluginPlatformVariant; - /// The `flutter build` build type. final String flutterBuildType; diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart index 15a0d6f1f3b2..13678d720a76 100644 --- a/script/tool/lib/src/common/core.dart +++ b/script/tool/lib/src/common/core.dart @@ -4,7 +4,6 @@ import 'package:colorize/colorize.dart'; import 'package:file/file.dart'; -import 'package:yaml/yaml.dart'; /// The signature for a print handler for commands that allow overriding the /// print destination. @@ -26,50 +25,19 @@ const String platformMacOS = 'macos'; const String platformWeb = 'web'; /// Key for windows platform. -/// -/// Note that this corresponds to the Win32 variant for flutter commands like -/// `build` and `run`, but is a general platform containing all Windows -/// variants for purposes of the `platform` section of a plugin pubspec). const String platformWindows = 'windows'; -/// Key for WinUWP platform. -/// -/// Note that UWP is a platform for the purposes of flutter commands like -/// `build` and `run`, but a variant of the `windows` platform for the purposes -/// of plugin pubspecs). -const String platformWinUwp = 'winuwp'; - -/// Key for Win32 variant of the Windows platform. -const String platformVariantWin32 = 'win32'; - -/// Key for UWP variant of the Windows platform. -/// -/// See the note on [platformWinUwp]. -const String platformVariantWinUwp = 'uwp'; - /// Key for enable experiment. const String kEnableExperiment = 'enable-experiment'; -/// Returns whether the given directory contains a Flutter package. -bool isFlutterPackage(FileSystemEntity entity) { +/// Returns whether the given directory is a Dart package. +bool isPackage(FileSystemEntity entity) { if (entity is! Directory) { return false; } - - try { - final File pubspecFile = entity.childFile('pubspec.yaml'); - final YamlMap pubspecYaml = - loadYaml(pubspecFile.readAsStringSync()) as YamlMap; - final YamlMap? dependencies = pubspecYaml['dependencies'] as YamlMap?; - if (dependencies == null) { - return false; - } - return dependencies.containsKey('flutter'); - } on FileSystemException { - return false; - } on YamlException { - return false; - } + // Per https://dart.dev/guides/libraries/create-library-packages#what-makes-a-library-package + return entity.childFile('pubspec.yaml').existsSync() && + entity.childDirectory('lib').existsSync(); } /// Prints `successMessage` in green. diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index b75aaa4a4a49..b48743be3170 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -10,7 +10,6 @@ import 'package:git/git.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'core.dart'; import 'plugin_command.dart'; diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index 081ce7f1e815..f33d3d73bb75 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:yaml/yaml.dart'; @@ -37,7 +36,6 @@ bool pluginSupportsPlatform( String platform, RepositoryPackage plugin, { PlatformSupport? requiredMode, - String? variant, }) { assert(platform == platformIOS || platform == platformAndroid || @@ -61,26 +59,6 @@ bool pluginSupportsPlatform( } } - // If a variant is specified, check for that variant. - if (variant != null) { - const String variantsKey = 'supportedVariants'; - if (platformEntry.containsKey(variantsKey)) { - if (!(platformEntry['supportedVariants']! as YamlList) - .contains(variant)) { - return false; - } - } else { - // Platforms with variants have a default variant when unspecified for - // backward compatibility. Must match the flutter tool logic. - const Map defaultVariants = { - platformWindows: platformVariantWin32, - }; - if (variant != defaultVariants[platform]) { - return false; - } - } - } - return true; } @@ -132,13 +110,8 @@ YamlMap? _readPlatformPubspecSectionForPlugin( /// section from [plugin]'s pubspec.yaml, or null if either it is not present, /// or the pubspec couldn't be read. YamlMap? _readPluginPubspecSection(RepositoryPackage package) { - final File pubspecFile = package.pubspecFile; - if (!pubspecFile.existsSync()) { - return null; - } - final YamlMap pubspecYaml = - loadYaml(pubspecFile.readAsStringSync()) as YamlMap; - final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; + final Pubspec pubspec = package.parsePubspec(); + final Map? flutterSection = pubspec.flutter; if (flutterSection == null) { return null; } diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index e0c4e4a83bfe..76519e040ae1 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -8,6 +8,8 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'core.dart'; +export 'package:pubspec_parse/pubspec_parse.dart' show Pubspec; + /// A package in the repository. // // TODO(stuartmorgan): Add more package-related info here, such as an on-demand @@ -48,6 +50,9 @@ class RepositoryPackage { /// The package's top-level pubspec.yaml. File get pubspecFile => directory.childFile('pubspec.yaml'); + /// The package's top-level README. + File get readmeFile => directory.childFile('README.md'); + late final Pubspec _parsedPubspec = Pubspec.parse(pubspecFile.readAsStringSync()); @@ -56,12 +61,24 @@ class RepositoryPackage { /// Caches for future use. Pubspec parsePubspec() => _parsedPubspec; + /// Returns true if the package depends on Flutter. + bool requiresFlutter() { + final Pubspec pubspec = parsePubspec(); + return pubspec.dependencies.containsKey('flutter'); + } + /// True if this appears to be a federated plugin package, according to /// repository conventions. bool get isFederated => directory.parent.basename != 'packages' && directory.basename.startsWith(directory.parent.basename); + /// True if this appears to be the app-facing package of a federated plugin, + /// according to repository conventions. + bool get isAppFacing => + directory.parent.basename != 'packages' && + directory.basename == directory.parent.basename; + /// True if this appears to be a platform interface package, according to /// repository conventions. bool get isPlatformInterface => @@ -82,7 +99,7 @@ class RepositoryPackage { if (!exampleDirectory.existsSync()) { return []; } - if (isFlutterPackage(exampleDirectory)) { + if (isPackage(exampleDirectory)) { return [RepositoryPackage(exampleDirectory)]; } // Only look at the subdirectories of the example directory if the example @@ -90,8 +107,8 @@ class RepositoryPackage { // example directory for other Dart packages. return exampleDirectory .listSync() - .where((FileSystemEntity entity) => isFlutterPackage(entity)) - // isFlutterPackage guarantees that the cast to Directory is safe. + .where((FileSystemEntity entity) => isPackage(entity)) + // isPackage guarantees that the cast to Directory is safe. .map((FileSystemEntity entity) => RepositoryPackage(entity as Directory)); } diff --git a/script/tool/lib/src/create_all_plugins_app_command.dart b/script/tool/lib/src/create_all_plugins_app_command.dart index 82f29bd501f3..6b44ab788786 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -147,6 +147,19 @@ class CreateAllPluginsAppCommand extends PluginCommand { } Future _genPubspecWithAllPlugins() async { + final RepositoryPackage buildAllApp = RepositoryPackage(appDirectory); + // Read the old pubspec file's Dart SDK version, in order to preserve it + // in the new file. The template sometimes relies on having opted in to + // specific language features via SDK version, so using a different one + // can cause compilation failures. + final Pubspec originalPubspec = buildAllApp.parsePubspec(); + const String dartSdkKey = 'sdk'; + final VersionConstraint dartSdkConstraint = + originalPubspec.environment?[dartSdkKey] ?? + VersionConstraint.compatibleWith( + Version.parse('2.12.0'), + ); + final Map pluginDeps = await _getValidPathDependencies(); final Pubspec pubspec = Pubspec( @@ -154,9 +167,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { description: 'Flutter app containing all 1st party plugins.', version: Version.parse('1.0.0+1'), environment: { - 'sdk': VersionConstraint.compatibleWith( - Version.parse('2.12.0'), - ), + dartSdkKey: dartSdkConstraint, }, dependencies: { 'flutter': SdkDependency('flutter'), @@ -166,8 +177,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { }, dependencyOverrides: pluginDeps, ); - final File pubspecFile = appDirectory.childFile('pubspec.yaml'); - pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); + buildAllApp.pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); } Future> _getValidPathDependencies() async { @@ -212,7 +222,12 @@ dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} for (final MapEntry entry in values.entries) { buffer.writeln(); if (entry.value is VersionConstraint) { - buffer.write(' ${entry.key}: ${entry.value}'); + String value = entry.value.toString(); + // Range constraints require quoting. + if (value.startsWith('>') || value.startsWith('<')) { + value = "'$value'"; + } + buffer.write(' ${entry.key}: $value'); } else if (entry.value is SdkDependency) { final SdkDependency dep = entry.value as SdkDependency; buffer.write(' ${entry.key}: \n sdk: ${dep.sdk}'); diff --git a/script/tool/lib/src/custom_test_command.dart b/script/tool/lib/src/custom_test_command.dart new file mode 100644 index 000000000000..cd9ac32606a6 --- /dev/null +++ b/script/tool/lib/src/custom_test_command.dart @@ -0,0 +1,86 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:platform/platform.dart'; + +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +const String _scriptName = 'run_tests.dart'; +const String _legacyScriptName = 'run_tests.sh'; + +/// A command to run custom, package-local tests on packages. +/// +/// This is an escape hatch for adding tests that this tooling doesn't support. +/// It should be used sparingly; prefer instead to add functionality to this +/// tooling to eliminate the need for bespoke tests. +class CustomTestCommand extends PackageLoopingCommand { + /// Creates a custom test command instance. + CustomTestCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform); + + @override + final String name = 'custom-test'; + + @override + final String description = 'Runs package-specific custom tests defined in ' + 'a package\'s tool/$_scriptName file.\n\n' + 'This command requires "dart" to be in your path.'; + + @override + Future runForPackage(RepositoryPackage package) async { + final File script = + package.directory.childDirectory('tool').childFile(_scriptName); + final File legacyScript = package.directory.childFile(_legacyScriptName); + String? customSkipReason; + bool ranTests = false; + + // Run the custom Dart script if presest. + if (script.existsSync()) { + // Ensure that dependencies are available. + final int pubGetExitCode = await processRunner.runAndStream( + 'dart', ['pub', 'get'], + workingDir: package.directory); + if (pubGetExitCode != 0) { + return PackageResult.fail( + ['Unable to get script dependencies']); + } + + final int testExitCode = await processRunner.runAndStream( + 'dart', ['run', 'tool/$_scriptName'], + workingDir: package.directory); + if (testExitCode != 0) { + return PackageResult.fail(); + } + ranTests = true; + } + + // Run the legacy script if present. + if (legacyScript.existsSync()) { + if (platform.isWindows) { + customSkipReason = '$_legacyScriptName is not supported on Windows. ' + 'Please migrate to $_scriptName.'; + } else { + final int exitCode = await processRunner.runAndStream( + legacyScript.path, [], + workingDir: package.directory); + if (exitCode != 0) { + return PackageResult.fail(); + } + ranTests = true; + } + } + + if (!ranTests) { + return PackageResult.skip(customSkipReason ?? 'No custom tests'); + } + + return PackageResult.success(); + } +} diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index d81153a0fefa..68ad9713a9b7 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -36,10 +36,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { argParser.addFlag(platformWeb, help: 'Runs the web implementation of the examples'); argParser.addFlag(platformWindows, - help: 'Runs the Windows (Win32) implementation of the examples'); - argParser.addFlag(platformWinUwp, - help: - 'Runs the UWP implementation of the examples [currently a no-op]'); + help: 'Runs the Windows implementation of the examples'); argParser.addOption( kEnableExperiment, defaultsTo: '', @@ -70,7 +67,6 @@ class DriveExamplesCommand extends PackageLoopingCommand { platformMacOS, platformWeb, platformWindows, - platformWinUwp, ]; final int platformCount = platformSwitches .where((String platform) => getBoolArg(platform)) @@ -85,10 +81,6 @@ class DriveExamplesCommand extends PackageLoopingCommand { throw ToolExit(_exitNoPlatformFlags); } - if (getBoolArg(platformWinUwp)) { - logWarning('Driving UWP applications is not yet supported'); - } - String? androidDevice; if (getBoolArg(platformAndroid)) { final List devices = await _getDevicesForPlatform('android'); @@ -126,14 +118,13 @@ class DriveExamplesCommand extends PackageLoopingCommand { ], if (getBoolArg(platformWindows)) platformWindows: ['-d', 'windows'], - // TODO(stuartmorgan): Check these flags once drive supports UWP: - // https://github.com/flutter/flutter/issues/82821 - if (getBoolArg(platformWinUwp)) platformWinUwp: ['-d', 'winuwp'], }; } @override Future runForPackage(RepositoryPackage package) async { + final bool isPlugin = isFlutterPlugin(package); + if (package.isPlatformInterface && !package.getSingleExampleDeprecated().directory.existsSync()) { // Platform interface packages generally aren't intended to have @@ -142,32 +133,23 @@ class DriveExamplesCommand extends PackageLoopingCommand { 'Platform interfaces are not expected to have integration tests.'); } - final List deviceFlags = []; - for (final MapEntry> entry - in _targetDeviceFlags.entries) { - final String platform = entry.key; - String? variant; - if (platform == platformWindows) { - variant = platformVariantWin32; - } else if (platform == platformWinUwp) { - variant = platformVariantWinUwp; - // TODO(stuartmorgan): Remove this once drive supports UWP. - // https://github.com/flutter/flutter/issues/82821 - return PackageResult.skip('Drive does not yet support UWP'); + // For plugin packages, skip if the plugin itself doesn't support any + // requested platform(s). + if (isPlugin) { + final Iterable requestedPlatforms = _targetDeviceFlags.keys; + final Iterable unsupportedPlatforms = requestedPlatforms.where( + (String platform) => !pluginSupportsPlatform(platform, package)); + for (final String platform in unsupportedPlatforms) { + print('Skipping unsupported platform $platform...'); } - if (pluginSupportsPlatform(platform, package, variant: variant)) { - deviceFlags.addAll(entry.value); - } else { - print('Skipping unsupported platform ${entry.key}...'); + if (unsupportedPlatforms.length == requestedPlatforms.length) { + return PackageResult.skip( + '${package.displayName} does not support any requested platform.'); } } - // If there is no supported target platform, skip the plugin. - if (deviceFlags.isEmpty) { - return PackageResult.skip( - '${package.displayName} does not support any requested platform.'); - } int examplesFound = 0; + int supportedExamplesFound = 0; bool testsRan = false; final List errors = []; for (final RepositoryPackage example in package.getExamples()) { @@ -175,6 +157,15 @@ class DriveExamplesCommand extends PackageLoopingCommand { final String exampleName = getRelativePosixPath(example.directory, from: packagesDir); + // Skip examples that don't support any requested platform(s). + final List deviceFlags = _deviceFlagsForExample(example); + if (deviceFlags.isEmpty) { + print( + 'Skipping $exampleName; does not support any requested platforms.'); + continue; + } + ++supportedExamplesFound; + final List drivers = await _getDrivers(example); if (drivers.isEmpty) { print('No driver tests found for $exampleName'); @@ -215,14 +206,41 @@ class DriveExamplesCommand extends PackageLoopingCommand { } } if (!testsRan) { - printError('No driver tests were run ($examplesFound example(s) found).'); - errors.add('No tests ran (use --exclude if this is intentional).'); + // It is an error for a plugin not to have integration tests, because that + // is the only way to test the method channel communication. + if (isPlugin) { + printError( + 'No driver tests were run ($examplesFound example(s) found).'); + errors.add('No tests ran (use --exclude if this is intentional).'); + } else { + return PackageResult.skip(supportedExamplesFound == 0 + ? 'No example supports requested platform(s).' + : 'No example is configured for driver tests.'); + } } return errors.isEmpty ? PackageResult.success() : PackageResult.fail(errors); } + /// Returns the device flags for the intersection of the requested platforms + /// and the platforms supported by [example]. + List _deviceFlagsForExample(RepositoryPackage example) { + final List deviceFlags = []; + for (final MapEntry> entry + in _targetDeviceFlags.entries) { + final String platform = entry.key; + if (example.directory.childDirectory(platform).existsSync()) { + deviceFlags.addAll(entry.value); + } else { + final String exampleName = + getRelativePosixPath(example.directory, from: packagesDir); + print('Skipping unsupported platform $platform for $exampleName'); + } + } + return deviceFlags; + } + Future> _getDevicesForPlatform(String platform) async { final List deviceIds = []; diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart index df9d86892e13..383637a9e896 100644 --- a/script/tool/lib/src/federation_safety_check_command.dart +++ b/script/tool/lib/src/federation_safety_check_command.dart @@ -8,7 +8,6 @@ import 'package:git/git.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/file_utils.dart'; diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index d2c129ff7b48..87e4c8b14861 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -3,7 +3,9 @@ // found in the LICENSE file. import 'package:file/file.dart'; +import 'package:git/git.dart'; import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'common/core.dart'; import 'common/plugin_command.dart'; @@ -105,7 +107,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /// Validates that code files have copyright and license blocks. class LicenseCheckCommand extends PluginCommand { /// Creates a new license check command for [packagesDir]. - LicenseCheckCommand(Directory packagesDir) : super(packagesDir); + LicenseCheckCommand(Directory packagesDir, + {Platform platform = const LocalPlatform(), GitDir? gitDir}) + : super(packagesDir, platform: platform, gitDir: gitDir); @override final String name = 'license-check'; @@ -116,7 +120,14 @@ class LicenseCheckCommand extends PluginCommand { @override Future run() async { - final Iterable allFiles = await _getAllFiles(); + // Create a set of absolute paths to submodule directories, with trailing + // separator, to do prefix matching with to test directory inclusion. + final Iterable submodulePaths = (await _getSubmoduleDirectories()) + .map( + (Directory dir) => '${dir.absolute.path}${platform.pathSeparator}'); + + final Iterable allFiles = (await _getAllFiles()).where( + (File file) => !submodulePaths.any(file.absolute.path.startsWith)); final Iterable codeFiles = allFiles.where((File file) => _codeFileExtensions.contains(p.extension(file.path)) && @@ -275,6 +286,24 @@ class LicenseCheckCommand extends PluginCommand { .where((FileSystemEntity entity) => entity is File) .map((FileSystemEntity file) => file as File) .toList(); + + // Returns the directories containing mapped submodules, if any. + Future> _getSubmoduleDirectories() async { + final List submodulePaths = []; + final Directory repoRoot = + packagesDir.fileSystem.directory((await gitDir).path); + final File submoduleSpec = repoRoot.childFile('.gitmodules'); + if (submoduleSpec.existsSync()) { + final RegExp pathLine = RegExp(r'path\s*=\s*(.*)'); + for (final String line in submoduleSpec.readAsLinesSync()) { + final RegExpMatch? match = pathLine.firstMatch(line); + if (match != null) { + submodulePaths.add(repoRoot.childDirectory(match.group(1)!.trim())); + } + } + } + return submodulePaths; + } } enum _LicenseFailureType { incorrectFirstParty, unknownThirdParty } diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 3e8f19b044dd..9c572ee270e0 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -12,6 +12,7 @@ import 'analyze_command.dart'; import 'build_examples_command.dart'; import 'common/core.dart'; import 'create_all_plugins_app_command.dart'; +import 'custom_test_command.dart'; import 'drive_examples_command.dart'; import 'federation_safety_check_command.dart'; import 'firebase_test_lab_command.dart'; @@ -25,7 +26,9 @@ import 'native_test_command.dart'; import 'publish_check_command.dart'; import 'publish_plugin_command.dart'; import 'pubspec_check_command.dart'; +import 'readme_check_command.dart'; import 'test_command.dart'; +import 'update_excerpts_command.dart'; import 'version_check_command.dart'; import 'xcode_analyze_command.dart'; @@ -50,6 +53,7 @@ void main(List args) { ..addCommand(AnalyzeCommand(packagesDir)) ..addCommand(BuildExamplesCommand(packagesDir)) ..addCommand(CreateAllPluginsAppCommand(packagesDir)) + ..addCommand(CustomTestCommand(packagesDir)) ..addCommand(DriveExamplesCommand(packagesDir)) ..addCommand(FederationSafetyCheckCommand(packagesDir)) ..addCommand(FirebaseTestLabCommand(packagesDir)) @@ -63,7 +67,9 @@ void main(List args) { ..addCommand(PublishCheckCommand(packagesDir)) ..addCommand(PublishPluginCommand(packagesDir)) ..addCommand(PubspecCheckCommand(packagesDir)) + ..addCommand(ReadmeCheckCommand(packagesDir)) ..addCommand(TestCommand(packagesDir)) + ..addCommand(UpdateExcerptsCommand(packagesDir)) ..addCommand(VersionCheckCommand(packagesDir)) ..addCommand(XcodeAnalyzeCommand(packagesDir)); diff --git a/script/tool/lib/src/make_deps_path_based_command.dart b/script/tool/lib/src/make_deps_path_based_command.dart index c09060310e97..370fc3559f71 100644 --- a/script/tool/lib/src/make_deps_path_based_command.dart +++ b/script/tool/lib/src/make_deps_path_based_command.dart @@ -3,19 +3,20 @@ // found in the LICENSE file. import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:git/git.dart'; import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/git_version_finder.dart'; import 'common/plugin_command.dart'; +import 'common/repository_package.dart'; const int _exitPackageNotFound = 3; const int _exitCannotUpdatePubspec = 4; +enum _RewriteOutcome { changed, noChangesNeeded, alreadyChanged } + /// Converts all dependencies on target packages to path-based dependencies. /// /// This is to allow for pre-publish testing of changes that could affect other @@ -48,6 +49,10 @@ class MakeDepsPathBasedCommand extends PluginCommand { static const String _targetDependenciesWithNonBreakingUpdatesArg = 'target-dependencies-with-non-breaking-updates'; + // The comment to add to temporary dependency overrides. + static const String _dependencyOverrideWarningComment = + '# FOR TESTING ONLY. DO NOT MERGE.'; + @override final String name = 'make-deps-path-based'; @@ -73,12 +78,19 @@ class MakeDepsPathBasedCommand extends PluginCommand { final String repoRootPath = (await gitDir).path; for (final File pubspec in await _getAllPubspecs()) { - if (await _addDependencyOverridesIfNecessary( - pubspec, localDependencyPackages)) { - // Print the relative path of the changed pubspec. - final String displayPath = p.posix.joinAll(path - .split(path.relative(pubspec.absolute.path, from: repoRootPath))); - print(' Modified $displayPath'); + final String displayPath = p.posix.joinAll( + path.split(path.relative(pubspec.absolute.path, from: repoRootPath))); + final _RewriteOutcome outcome = await _addDependencyOverridesIfNecessary( + pubspec, localDependencyPackages); + switch (outcome) { + case _RewriteOutcome.changed: + print(' Modified $displayPath'); + break; + case _RewriteOutcome.alreadyChanged: + print(' Skipped $displayPath - Already rewritten'); + break; + case _RewriteOutcome.noChangesNeeded: + break; } } } @@ -125,16 +137,18 @@ class MakeDepsPathBasedCommand extends PluginCommand { /// If [pubspecFile] has any dependencies on packages in [localDependencies], /// adds dependency_overrides entries to redirect them to the local version /// using path-based dependencies. - /// - /// Returns true if any changes were made. - Future _addDependencyOverridesIfNecessary(File pubspecFile, + Future<_RewriteOutcome> _addDependencyOverridesIfNecessary(File pubspecFile, Map localDependencies) async { final String pubspecContents = pubspecFile.readAsStringSync(); final Pubspec pubspec = Pubspec.parse(pubspecContents); - // Fail if there are any dependency overrides already. If support for that - // is needed at some point, it can be added, but currently it's not and - // relying on that makes the logic here much simpler. + // Fail if there are any dependency overrides already, other than ones + // created by this script. If support for that is needed at some point, it + // can be added, but currently it's not and relying on that makes the logic + // here much simpler. if (pubspec.dependencyOverrides.isNotEmpty) { + if (pubspecContents.contains(_dependencyOverrideWarningComment)) { + return _RewriteOutcome.alreadyChanged; + } printError( 'Plugins with dependency overrides are not currently supported.'); throw ToolExit(_exitCannotUpdatePubspec); @@ -158,7 +172,7 @@ class MakeDepsPathBasedCommand extends PluginCommand { String newPubspecContents = pubspecContents + ''' -# FOR TESTING ONLY. DO NOT MERGE. +$_dependencyOverrideWarningComment dependency_overrides: '''; for (final String packageName in packagesToOverride) { @@ -175,9 +189,9 @@ dependency_overrides: '''; } pubspecFile.writeAsStringSync(newPubspecContents); - return true; + return _RewriteOutcome.changed; } - return false; + return _RewriteOutcome.noChangesNeeded; } /// Returns all pubspecs anywhere under the packages directory. diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 8fd96b818c1d..b6b83dabcb49 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -10,7 +10,6 @@ import 'package:file/file.dart'; import 'package:http/http.dart' as http; import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 28d17a3a2487..05f0afd0c06f 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -13,7 +13,6 @@ import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml/yaml.dart'; import 'common/core.dart'; diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 2c27c91e0490..654675ebb858 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -5,7 +5,6 @@ import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:platform/platform.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml/yaml.dart'; import 'common/core.dart'; diff --git a/script/tool/lib/src/readme_check_command.dart b/script/tool/lib/src/readme_check_command.dart new file mode 100644 index 000000000000..0cb64920dea4 --- /dev/null +++ b/script/tool/lib/src/readme_check_command.dart @@ -0,0 +1,226 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:platform/platform.dart'; +import 'package:yaml/yaml.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +/// A command to enforce README conventions across the repository. +class ReadmeCheckCommand extends PackageLoopingCommand { + /// Creates an instance of the README check command. + ReadmeCheckCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + GitDir? gitDir, + }) : super( + packagesDir, + processRunner: processRunner, + platform: platform, + gitDir: gitDir, + ) { + argParser.addFlag(_requireExcerptsArg, + help: 'Require that Dart code blocks be managed by code-excerpt.'); + } + + static const String _requireExcerptsArg = 'require-excerpts'; + + // Standardized capitalizations for platforms that a plugin can support. + static const Map _standardPlatformNames = { + 'android': 'Android', + 'ios': 'iOS', + 'linux': 'Linux', + 'macos': 'macOS', + 'web': 'Web', + 'windows': 'Windows', + }; + + @override + final String name = 'readme-check'; + + @override + final String description = + 'Checks that READMEs follow repository conventions.'; + + @override + bool get hasLongOutput => false; + + @override + Future runForPackage(RepositoryPackage package) async { + final File readme = package.readmeFile; + + if (!readme.existsSync()) { + return PackageResult.fail(['Missing README.md']); + } + + final List errors = []; + + final Pubspec pubspec = package.parsePubspec(); + final bool isPlugin = pubspec.flutter?['plugin'] != null; + + final List readmeLines = package.readmeFile.readAsLinesSync(); + + final String? blockValidationError = _validateCodeBlocks(readmeLines); + if (blockValidationError != null) { + errors.add(blockValidationError); + } + + if (isPlugin && (!package.isFederated || package.isAppFacing)) { + final String? error = _validateSupportedPlatforms(readmeLines, pubspec); + if (error != null) { + errors.add(error); + } + } + + return errors.isEmpty + ? PackageResult.success() + : PackageResult.fail(errors); + } + + /// Validates that code blocks (``` ... ```) follow repository standards. + String? _validateCodeBlocks(List readmeLines) { + final RegExp codeBlockDelimiterPattern = RegExp(r'^\s*```\s*([^ ]*)\s*'); + final List missingLanguageLines = []; + final List missingExcerptLines = []; + bool inBlock = false; + for (int i = 0; i < readmeLines.length; ++i) { + final RegExpMatch? match = + codeBlockDelimiterPattern.firstMatch(readmeLines[i]); + if (match == null) { + continue; + } + if (inBlock) { + inBlock = false; + continue; + } + inBlock = true; + + final int humanReadableLineNumber = i + 1; + + // Ensure that there's a language tag. + final String infoString = match[1] ?? ''; + if (infoString.isEmpty) { + missingLanguageLines.add(humanReadableLineNumber); + continue; + } + + // Check for code-excerpt usage if requested. + if (getBoolArg(_requireExcerptsArg) && infoString == 'dart') { + const String excerptTagStart = ' ' + 'tag on the previous line, and ensure that a build.excerpt.yaml is ' + 'configured for the source example.\n'); + errorSummary ??= 'Missing code-excerpt management for code block'; + } + + return errorSummary; + } + + /// Validates that the plugin has a supported platforms table following the + /// expected format, returning an error string if any issues are found. + String? _validateSupportedPlatforms( + List readmeLines, Pubspec pubspec) { + // Example table following expected format: + // | | Android | iOS | Web | + // |----------------|---------|----------|------------------------| + // | **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] | + final int detailsLineNumber = readmeLines + .indexWhere((String line) => line.startsWith('| **Support**')); + if (detailsLineNumber == -1) { + return 'No OS support table found'; + } + final int osLineNumber = detailsLineNumber - 2; + if (osLineNumber < 0 || !readmeLines[osLineNumber].startsWith('|')) { + return 'OS support table does not have the expected header format'; + } + + // Utility method to convert an iterable of strings to a case-insensitive + // sorted, comma-separated string of its elements. + String sortedListString(Iterable entries) { + final List entryList = entries.toList(); + entryList.sort( + (String a, String b) => a.toLowerCase().compareTo(b.toLowerCase())); + return entryList.join(', '); + } + + // Validate that the supported OS lists match. + final dynamic platformsEntry = pubspec.flutter!['plugin']!['platforms']; + if (platformsEntry == null) { + logWarning('Plugin not support any platforms'); + return null; + } + final YamlMap platformSupportMaps = platformsEntry as YamlMap; + final Set actuallySupportedPlatform = + platformSupportMaps.keys.toSet().cast(); + final Iterable documentedPlatforms = readmeLines[osLineNumber] + .split('|') + .map((String entry) => entry.trim()) + .where((String entry) => entry.isNotEmpty); + final Set documentedPlatformsLowercase = + documentedPlatforms.map((String entry) => entry.toLowerCase()).toSet(); + if (actuallySupportedPlatform.length != documentedPlatforms.length || + actuallySupportedPlatform + .intersection(documentedPlatformsLowercase) + .length != + actuallySupportedPlatform.length) { + printError(''' +${indentation}OS support table does not match supported platforms: +${indentation * 2}Actual: ${sortedListString(actuallySupportedPlatform)} +${indentation * 2}Documented: ${sortedListString(documentedPlatformsLowercase)} +'''); + return 'Incorrect OS support table'; + } + + // Enforce a standard set of capitalizations for the OS headings. + final Iterable incorrectCapitalizations = documentedPlatforms + .toSet() + .difference(_standardPlatformNames.values.toSet()); + if (incorrectCapitalizations.isNotEmpty) { + final Iterable expectedVersions = incorrectCapitalizations + .map((String name) => _standardPlatformNames[name.toLowerCase()]!); + printError(''' +${indentation}Incorrect OS capitalization: ${sortedListString(incorrectCapitalizations)} +${indentation * 2}Please use standard capitalizations: ${sortedListString(expectedVersions)} +'''); + return 'Incorrect OS support formatting'; + } + + // TODO(stuartmorgan): Add validation that the minimums in the table are + // consistent with what the current implementations require. See + // https://github.com/flutter/flutter/issues/84200 + return null; + } +} diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index 2c5dd9934b45..a1a995dbd88f 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -36,6 +36,9 @@ class TestCommand extends PackageLoopingCommand { final String description = 'Runs the Dart tests for all packages.\n\n' 'This command requires "flutter" to be in your path.'; + @override + bool get includeSubpackages => true; + @override Future runForPackage(RepositoryPackage package) async { if (!package.directory.childDirectory('test').existsSync()) { @@ -43,7 +46,7 @@ class TestCommand extends PackageLoopingCommand { } bool passed; - if (isFlutterPackage(package.directory)) { + if (package.requiresFlutter()) { passed = await _runFlutterTests(package); } else { passed = await _runDartTests(package); @@ -88,7 +91,6 @@ class TestCommand extends PackageLoopingCommand { exitCode = await processRunner.runAndStream( 'dart', [ - 'pub', 'run', if (experiment.isNotEmpty) '--enable-experiment=$experiment', 'test', diff --git a/script/tool/lib/src/update_excerpts_command.dart b/script/tool/lib/src/update_excerpts_command.dart new file mode 100644 index 000000000000..320a3c596323 --- /dev/null +++ b/script/tool/lib/src/update_excerpts_command.dart @@ -0,0 +1,225 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:git/git.dart'; +import 'package:platform/platform.dart'; +import 'package:yaml/yaml.dart'; +import 'package:yaml_edit/yaml_edit.dart'; + +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +/// A command to update README code excerpts from code files. +class UpdateExcerptsCommand extends PackageLoopingCommand { + /// Creates a excerpt updater command instance. + UpdateExcerptsCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + GitDir? gitDir, + }) : super( + packagesDir, + processRunner: processRunner, + platform: platform, + gitDir: gitDir, + ) { + argParser.addFlag(_failOnChangeFlag, hide: true); + } + + static const String _failOnChangeFlag = 'fail-on-change'; + + static const String _buildRunnerConfigName = 'excerpt'; + // The name of the build_runner configuration file that will be in an example + // directory if the package is set up to use `code-excerpt`. + static const String _buildRunnerConfigFile = + 'build.$_buildRunnerConfigName.yaml'; + + // The relative directory path to put the extracted excerpt yaml files. + static const String _excerptOutputDir = 'excerpts'; + + // The filename to store the pre-modification copy of the pubspec. + static const String _originalPubspecFilename = + 'pubspec.plugin_tools_original.yaml'; + + @override + final String name = 'update-excerpts'; + + @override + final String description = 'Updates code excerpts in README.md files, based ' + 'on code from code files, via code-excerpt'; + + @override + Future runForPackage(RepositoryPackage package) async { + final Iterable configuredExamples = package + .getExamples() + .where((RepositoryPackage example) => + example.directory.childFile(_buildRunnerConfigFile).existsSync()); + + if (configuredExamples.isEmpty) { + return PackageResult.skip( + 'No $_buildRunnerConfigFile found in example(s).'); + } + + final Directory repoRoot = + packagesDir.fileSystem.directory((await gitDir).path); + + for (final RepositoryPackage example in configuredExamples) { + _addSubmoduleDependencies(example, repoRoot: repoRoot); + + try { + // Ensure that dependencies are available. + final int pubGetExitCode = await processRunner.runAndStream( + 'dart', ['pub', 'get'], + workingDir: example.directory); + if (pubGetExitCode != 0) { + return PackageResult.fail( + ['Unable to get script dependencies']); + } + + // Update the excerpts. + if (!await _extractSnippets(example)) { + return PackageResult.fail(['Unable to extract excerpts']); + } + if (!await _injectSnippets(example, targetPackage: package)) { + return PackageResult.fail(['Unable to inject excerpts']); + } + } finally { + // Clean up the pubspec changes and extracted excerpts directory. + _undoPubspecChanges(example); + final Directory excerptDirectory = + example.directory.childDirectory(_excerptOutputDir); + if (excerptDirectory.existsSync()) { + excerptDirectory.deleteSync(recursive: true); + } + } + } + + if (getBoolArg(_failOnChangeFlag)) { + final String? stateError = await _validateRepositoryState(); + if (stateError != null) { + printError('README.md is out of sync with its source excerpts.\n\n' + 'If you edited code in README.md directly, you should instead edit ' + 'the example source files. If you edited source files, run the ' + 'repository tooling\'s "$name" command on this package, and update ' + 'your PR with the resulting changes.'); + return PackageResult.fail([stateError]); + } + } + + return PackageResult.success(); + } + + /// Runs the extraction step to create the excerpt files for the given + /// example, returning true on success. + Future _extractSnippets(RepositoryPackage example) async { + final int exitCode = await processRunner.runAndStream( + 'dart', + [ + 'run', + 'build_runner', + 'build', + '--config', + _buildRunnerConfigName, + '--output', + _excerptOutputDir, + '--delete-conflicting-outputs', + ], + workingDir: example.directory); + return exitCode == 0; + } + + /// Runs the injection step to update [targetPackage]'s README with the latest + /// excerpts from [example], returning true on success. + Future _injectSnippets( + RepositoryPackage example, { + required RepositoryPackage targetPackage, + }) async { + final String relativeReadmePath = + getRelativePosixPath(targetPackage.readmeFile, from: example.directory); + final int exitCode = await processRunner.runAndStream( + 'dart', + [ + 'run', + 'code_excerpt_updater', + '--write-in-place', + '--yaml', + '--no-escape-ng-interpolation', + relativeReadmePath, + ], + workingDir: example.directory); + return exitCode == 0; + } + + /// Adds `code_excerpter` and `code_excerpt_updater` to [package]'s + /// `dev_dependencies` using path-based references to the submodule copies. + /// + /// This is done on the fly rather than being checked in so that: + /// - Just building examples don't require everyone to check out submodules. + /// - Examples can be analyzed/built even on versions of Flutter that these + /// submodules do not support. + void _addSubmoduleDependencies(RepositoryPackage package, + {required Directory repoRoot}) { + final String pubspecContents = package.pubspecFile.readAsStringSync(); + // Save aside a copy of the current pubspec state. This allows restoration + // to the previous state regardless of its git status at the time the script + // ran. + package.directory + .childFile(_originalPubspecFilename) + .writeAsStringSync(pubspecContents); + + // Update the actual pubspec. + final YamlEditor editablePubspec = YamlEditor(pubspecContents); + const String devDependenciesKey = 'dev_dependencies'; + final YamlNode root = editablePubspec.parseAt([]); + // Ensure that there's a `dev_dependencies` entry to update. + if ((root as YamlMap)[devDependenciesKey] == null) { + editablePubspec.update(['dev_dependencies'], YamlMap()); + } + final Set submoduleDependencies = { + 'code_excerpter', + 'code_excerpt_updater', + }; + final String relativeRootPath = + getRelativePosixPath(repoRoot, from: package.directory); + for (final String dependency in submoduleDependencies) { + editablePubspec.update([ + devDependenciesKey, + dependency + ], { + 'path': '$relativeRootPath/site-shared/packages/$dependency' + }); + } + package.pubspecFile.writeAsStringSync(editablePubspec.toString()); + } + + /// Restores the version of the pubspec that was present before running + /// [_addSubmoduleDependencies]. + void _undoPubspecChanges(RepositoryPackage package) { + package.directory + .childFile(_originalPubspecFilename) + .renameSync(package.pubspecFile.path); + } + + /// Checks the git state, returning an error string unless nothing has + /// changed. + Future _validateRepositoryState() async { + final io.ProcessResult modifiedFiles = await processRunner.run( + 'git', + ['ls-files', '--modified'], + workingDir: packagesDir, + logOnError: true, + ); + if (modifiedFiles.exitCode != 0) { + return 'Unable to determine local file state'; + } + + final String stdout = modifiedFiles.stdout as String; + return stdout.trim().isEmpty ? null : 'Snippets are out of sync'; + } +} diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index fcaea335920f..f69611f5a06d 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -9,7 +9,6 @@ import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/git_version_finder.dart'; diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 93a1a87ca337..32bfc1b62281 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.8.1 +version: 0.8.5 dependencies: args: ^2.1.0 @@ -21,6 +21,7 @@ dependencies: test: ^1.17.3 uuid: ^3.0.4 yaml: ^3.1.0 + yaml_edit: ^2.0.2 dev_dependencies: build_runner: ^2.0.3 diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 29a879071657..2bdb1bc0c2ba 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -313,7 +313,7 @@ void main() { }); test( - 'building for win32 when plugin is not set up for Windows results in no-op', + 'building for Windows when plugin is not set up for Windows results in no-op', () async { mockPlatform.isWindows = true; createFakePlugin('plugin', packagesDir); @@ -325,7 +325,7 @@ void main() { output, containsAllInOrder([ contains('Running for plugin'), - contains('Win32 is not supported by this plugin'), + contains('Windows is not supported by this plugin'), ]), ); @@ -334,7 +334,7 @@ void main() { expect(processRunner.recordedCalls, orderedEquals([])); }); - test('building for win32', () async { + test('building for Windows', () async { mockPlatform.isWindows = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, platformSupport: { @@ -350,7 +350,7 @@ void main() { expect( output, containsAllInOrder([ - '\nBUILDING plugin/example for Win32 (windows)', + '\nBUILDING plugin/example for Windows', ]), ); @@ -364,88 +364,6 @@ void main() { ])); }); - test('building for UWP when plugin does not support UWP is a no-op', - () async { - createFakePlugin('plugin', packagesDir); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--winuwp']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('UWP is not supported by this plugin'), - ]), - ); - - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('building for UWP', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.federated, - variants: [platformVariantWinUwp]), - }); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--winuwp']); - - expect( - output, - containsAllInOrder([ - contains('BUILDING plugin/example for UWP (winuwp)'), - ]), - ); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['build', 'winuwp'], pluginExampleDirectory.path), - ])); - }); - - test('building for UWP creates a folder if necessary', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.federated, - variants: [platformVariantWinUwp]), - }); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--winuwp']); - - expect( - output, - contains('Creating temporary winuwp folder'), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['create', '--platforms=winuwp', '.'], - pluginExampleDirectory.path), - ProcessCall(getFlutterCommand(mockPlatform), - const ['build', 'winuwp'], pluginExampleDirectory.path), - ])); - }); - test( 'building for Android when plugin is not set up for Android results in no-op', () async { diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart index cedd40acb7d6..af5cb7dfe4c6 100644 --- a/script/tool/test/common/plugin_utils_test.dart +++ b/script/tool/test/common/plugin_utils_test.dart @@ -193,85 +193,6 @@ void main() { requiredMode: PlatformSupport.inline), isFalse); }); - - test('windows without variants is only win32', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }, - )); - - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWin32), - isTrue); - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWinUwp), - isFalse); - }); - - test('windows with both variants matches win32 and winuwp', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - platformWindows: const PlatformDetails( - PlatformSupport.federated, - variants: [platformVariantWin32, platformVariantWinUwp], - ), - })); - - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWin32), - isTrue); - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWinUwp), - isTrue); - }); - - test('win32 plugin is only win32', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - platformWindows: const PlatformDetails( - PlatformSupport.federated, - variants: [platformVariantWin32], - ), - })); - - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWin32), - isTrue); - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWinUwp), - isFalse); - }); - - test('winup plugin is only winuwp', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.federated, - variants: [platformVariantWinUwp]), - }, - )); - - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWin32), - isFalse); - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWinUwp), - isTrue); - }); }); group('pluginHasNativeCodeForPlatform', () { diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart index 29e3b5832127..2d0e11475c36 100644 --- a/script/tool/test/common/repository_package_test.dart +++ b/script/tool/test/common/repository_package_test.dart @@ -5,7 +5,6 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/repository_package.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:test/test.dart'; import '../util.dart'; @@ -97,7 +96,7 @@ void main() { }); group('getExamples', () { - test('handles a single example', () async { + test('handles a single Flutter example', () async { final Directory plugin = createFakePlugin('a_plugin', packagesDir); final List examples = @@ -107,7 +106,7 @@ void main() { expect(examples[0].path, plugin.childDirectory('example').path); }); - test('handles multiple examples', () async { + test('handles multiple Flutter examples', () async { final Directory plugin = createFakePlugin('a_plugin', packagesDir, examples: ['example1', 'example2']); @@ -120,12 +119,37 @@ void main() { expect(examples[1].path, plugin.childDirectory('example').childDirectory('example2').path); }); + + test('handles a single non-Flutter example', () async { + final Directory package = createFakePackage('a_package', packagesDir); + + final List examples = + RepositoryPackage(package).getExamples().toList(); + + expect(examples.length, 1); + expect(examples[0].path, package.childDirectory('example').path); + }); + + test('handles multiple non-Flutter examples', () async { + final Directory package = createFakePackage('a_package', packagesDir, + examples: ['example1', 'example2']); + + final List examples = + RepositoryPackage(package).getExamples().toList(); + + expect(examples.length, 2); + expect(examples[0].path, + package.childDirectory('example').childDirectory('example1').path); + expect(examples[1].path, + package.childDirectory('example').childDirectory('example2').path); + }); }); group('federated plugin queries', () { test('all return false for a simple plugin', () { final Directory plugin = createFakePlugin('a_plugin', packagesDir); expect(RepositoryPackage(plugin).isFederated, false); + expect(RepositoryPackage(plugin).isAppFacing, false); expect(RepositoryPackage(plugin).isPlatformInterface, false); expect(RepositoryPackage(plugin).isFederated, false); }); @@ -134,6 +158,7 @@ void main() { final Directory plugin = createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); expect(RepositoryPackage(plugin).isFederated, true); + expect(RepositoryPackage(plugin).isAppFacing, true); expect(RepositoryPackage(plugin).isPlatformInterface, false); expect(RepositoryPackage(plugin).isPlatformImplementation, false); }); @@ -142,6 +167,7 @@ void main() { final Directory plugin = createFakePlugin('a_plugin_platform_interface', packagesDir.childDirectory('a_plugin')); expect(RepositoryPackage(plugin).isFederated, true); + expect(RepositoryPackage(plugin).isAppFacing, false); expect(RepositoryPackage(plugin).isPlatformInterface, true); expect(RepositoryPackage(plugin).isPlatformImplementation, false); }); @@ -152,6 +178,7 @@ void main() { final Directory plugin = createFakePlugin( 'a_plugin_foo', packagesDir.childDirectory('a_plugin')); expect(RepositoryPackage(plugin).isFederated, true); + expect(RepositoryPackage(plugin).isAppFacing, false); expect(RepositoryPackage(plugin).isPlatformInterface, false); expect(RepositoryPackage(plugin).isPlatformImplementation, true); }); @@ -175,4 +202,18 @@ void main() { expect(pubspec.name, 'a_plugin'); }); }); + + group('requiresFlutter', () { + test('returns true for Flutter package', () async { + final Directory package = + createFakePackage('a_package', packagesDir, isFlutter: true); + expect(RepositoryPackage(package).requiresFlutter(), true); + }); + + test('returns false for non-Flutter package', () async { + final Directory package = + createFakePackage('a_package', packagesDir, isFlutter: false); + expect(RepositoryPackage(package).requiresFlutter(), false); + }); + }); } diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index 0066cc53f61a..9e2ee29326d7 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -2,10 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io' as io; + import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:flutter_plugin_tools/src/create_all_plugins_app_command.dart'; +import 'package:platform/platform.dart'; import 'package:test/test.dart'; import 'util.dart'; @@ -76,14 +80,31 @@ void main() { ])); }); - test('pubspec is compatible with null-safe app code', () async { + test('pubspec preserves existing Dart SDK version', () async { + const String baselineProjectName = 'baseline'; + final Directory baselineProjectDirectory = + testRoot.childDirectory(baselineProjectName); + io.Process.runSync( + getFlutterCommand(const LocalPlatform()), + [ + 'create', + '--template=app', + '--project-name=$baselineProjectName', + baselineProjectDirectory.path, + ], + ); + final Pubspec baselinePubspec = + RepositoryPackage(baselineProjectDirectory).parsePubspec(); + createFakePlugin('plugina', packagesDir); await runCapturingPrint(runner, ['all-plugins-app']); - final String pubspec = - command.appDirectory.childFile('pubspec.yaml').readAsStringSync(); + final Pubspec generatedPubspec = + RepositoryPackage(command.appDirectory).parsePubspec(); - expect(pubspec, contains(RegExp('sdk:\\s*(?:["\']>=|[^])2\\.12\\.'))); + const String dartSdkKey = 'sdk'; + expect(generatedPubspec.environment?[dartSdkKey], + baselinePubspec.environment?[dartSdkKey]); }); test('handles --output-dir', () async { diff --git a/script/tool/test/custom_test_command_test.dart b/script/tool/test/custom_test_command_test.dart new file mode 100644 index 000000000000..bc30d9a1d2e3 --- /dev/null +++ b/script/tool/test/custom_test_command_test.dart @@ -0,0 +1,324 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/custom_test_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late RecordingProcessRunner processRunner; + late CommandRunner runner; + + group('posix', () { + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final CustomTestCommand analyzeCommand = CustomTestCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'custom_test_command', 'Test for custom_test_command'); + runner.addCommand(analyzeCommand); + }); + + test('runs both new and legacy when both are present', () async { + final Directory package = + createFakePlugin('a_package', packagesDir, extraFiles: [ + 'tool/run_tests.dart', + 'run_tests.sh', + ]); + + final List output = + await runCapturingPrint(runner, ['custom-test']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall(package.childFile('run_tests.sh').path, + const [], package.path), + ProcessCall('dart', const ['run', 'tool/run_tests.dart'], + package.path), + ])); + + expect( + output, + containsAllInOrder([ + contains('Ran for 1 package(s)'), + ])); + }); + + test('runs when only new is present', () async { + final Directory package = createFakePlugin('a_package', packagesDir, + extraFiles: ['tool/run_tests.dart']); + + final List output = + await runCapturingPrint(runner, ['custom-test']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall('dart', const ['run', 'tool/run_tests.dart'], + package.path), + ])); + + expect( + output, + containsAllInOrder([ + contains('Ran for 1 package(s)'), + ])); + }); + + test('runs pub get before running Dart test script', () async { + final Directory package = createFakePlugin('a_package', packagesDir, + extraFiles: ['tool/run_tests.dart']); + + await runCapturingPrint(runner, ['custom-test']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall('dart', const ['pub', 'get'], package.path), + ProcessCall('dart', const ['run', 'tool/run_tests.dart'], + package.path), + ])); + }); + + test('runs when only legacy is present', () async { + final Directory package = createFakePlugin('a_package', packagesDir, + extraFiles: ['run_tests.sh']); + + final List output = + await runCapturingPrint(runner, ['custom-test']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall(package.childFile('run_tests.sh').path, + const [], package.path), + ])); + + expect( + output, + containsAllInOrder([ + contains('Ran for 1 package(s)'), + ])); + }); + + test('skips when neither is present', () async { + createFakePlugin('a_package', packagesDir); + + final List output = + await runCapturingPrint(runner, ['custom-test']); + + expect(processRunner.recordedCalls, isEmpty); + + expect( + output, + containsAllInOrder([ + contains('Skipped 1 package(s)'), + ])); + }); + + test('fails if new fails', () async { + createFakePlugin('a_package', packagesDir, extraFiles: [ + 'tool/run_tests.dart', + 'run_tests.sh', + ]); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 0), // pub get + MockProcess(exitCode: 1), // test script + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['custom-test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package') + ])); + }); + + test('fails if pub get fails', () async { + createFakePlugin('a_package', packagesDir, extraFiles: [ + 'tool/run_tests.dart', + 'run_tests.sh', + ]); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['custom-test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package:\n' + ' Unable to get script dependencies') + ])); + }); + + test('fails if legacy fails', () async { + final Directory package = + createFakePlugin('a_package', packagesDir, extraFiles: [ + 'tool/run_tests.dart', + 'run_tests.sh', + ]); + + processRunner.mockProcessesForExecutable[ + package.childFile('run_tests.sh').path] = [ + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['custom-test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package') + ])); + }); + }); + + group('Windows', () { + setUp(() { + fileSystem = MemoryFileSystem(style: FileSystemStyle.windows); + mockPlatform = MockPlatform(isWindows: true); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final CustomTestCommand analyzeCommand = CustomTestCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'custom_test_command', 'Test for custom_test_command'); + runner.addCommand(analyzeCommand); + }); + + test('runs new and skips old when both are present', () async { + final Directory package = + createFakePlugin('a_package', packagesDir, extraFiles: [ + 'tool/run_tests.dart', + 'run_tests.sh', + ]); + + final List output = + await runCapturingPrint(runner, ['custom-test']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall('dart', const ['run', 'tool/run_tests.dart'], + package.path), + ])); + + expect( + output, + containsAllInOrder([ + contains('Ran for 1 package(s)'), + ])); + }); + + test('runs when only new is present', () async { + final Directory package = createFakePlugin('a_package', packagesDir, + extraFiles: ['tool/run_tests.dart']); + + final List output = + await runCapturingPrint(runner, ['custom-test']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall('dart', const ['run', 'tool/run_tests.dart'], + package.path), + ])); + + expect( + output, + containsAllInOrder([ + contains('Ran for 1 package(s)'), + ])); + }); + + test('skips package when only legacy is present', () async { + createFakePlugin('a_package', packagesDir, + extraFiles: ['run_tests.sh']); + + final List output = + await runCapturingPrint(runner, ['custom-test']); + + expect(processRunner.recordedCalls, isEmpty); + + expect( + output, + containsAllInOrder([ + contains('run_tests.sh is not supported on Windows'), + contains('Skipped 1 package(s)'), + ])); + }); + + test('fails if new fails', () async { + createFakePlugin('a_package', packagesDir, extraFiles: [ + 'tool/run_tests.dart', + 'run_tests.sh', + ]); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 0), // pub get + MockProcess(exitCode: 1), // test script + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['custom-test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package') + ])); + }); + }); +} diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 9372c571b6f7..214efb420227 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -128,6 +128,7 @@ void main() { extraFiles: [ 'example/test_driver/integration_test.dart', 'example/integration_test/foo_test.dart', + 'example/ios/ios.m', ], platformSupport: { platformIOS: const PlatformDetails(PlatformSupport.inline), @@ -193,6 +194,8 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', + 'example/android/android.java', + 'example/ios/ios.m', ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), @@ -243,6 +246,8 @@ void main() { packagesDir, extraFiles: [ 'example/test_driver/plugin_test.dart', + 'example/android/android.java', + 'example/ios/ios.m', ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), @@ -276,6 +281,8 @@ void main() { packagesDir, extraFiles: [ 'example/lib/main.dart', + 'example/android/android.java', + 'example/ios/ios.m', ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), @@ -312,6 +319,8 @@ void main() { 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', 'example/integration_test/ignore_me.dart', + 'example/android/android.java', + 'example/ios/ios.m', ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), @@ -398,6 +407,7 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', + 'example/linux/linux.cc', ], platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), @@ -542,6 +552,7 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', + 'example/web/index.html', ], platformSupport: { platformWeb: const PlatformDetails(PlatformSupport.inline), @@ -591,6 +602,7 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', + 'example/web/index.html', ], platformSupport: { platformWeb: const PlatformDetails(PlatformSupport.inline), @@ -668,6 +680,7 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', + 'example/windows/windows.cpp', ], platformSupport: { platformWindows: const PlatformDetails(PlatformSupport.inline), @@ -708,40 +721,6 @@ void main() { ])); }); - test('driving UWP is a no-op', () async { - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - ], - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline, - variants: [platformVariantWinUwp]), - }, - ); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--winuwp', - ]); - - expect( - output, - containsAllInOrder([ - contains('Driving UWP applications is not yet supported'), - contains('Running for plugin'), - contains('SKIPPING: Drive does not yet support UWP'), - contains('No issues found!'), - ]), - ); - - // Output should be empty since running drive-examples --windows on a - // non-Windows plugin is a no-op. - expect(processRunner.recordedCalls, []); - }); - test('driving on an Android plugin', () async { final Directory pluginDirectory = createFakePlugin( 'plugin', @@ -749,6 +728,7 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', + 'example/android/android.java', ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), @@ -887,6 +867,8 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', + 'example/android/android.java', + 'example/ios/ios.m', ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), @@ -961,6 +943,7 @@ void main() { extraFiles: [ 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', + 'example/web/index.html', ], platformSupport: { platformWeb: const PlatformDetails(PlatformSupport.inline), @@ -993,6 +976,7 @@ void main() { packagesDir, extraFiles: [ 'example/test_driver/integration_test.dart', + 'example/web/index.html', ], platformSupport: { platformWeb: const PlatformDetails(PlatformSupport.inline), @@ -1029,6 +1013,7 @@ void main() { 'example/test_driver/integration_test.dart', 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', + 'example/macos/macos.swift', ], platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), @@ -1094,5 +1079,147 @@ void main() { pluginExampleDirectory.path), ])); }); + + group('packages', () { + test('can be driven', () async { + final Directory package = + createFakePackage('a_package', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/test_driver/integration_test.dart', + 'example/web/index.html', + ]); + final Directory exampleDirectory = package.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + 'web-server', + '--web-port=7357', + '--browser-name=chrome', + '--driver', + 'test_driver/integration_test.dart', + '--target', + 'integration_test/foo_test.dart' + ], + exampleDirectory.path), + ])); + }); + + test('are skipped when example does not support platform', () async { + createFakePackage('a_package', packagesDir, + isFlutter: true, + extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/test_driver/integration_test.dart', + ]); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package'), + contains('Skipping a_package/example; does not support any ' + 'requested platforms'), + contains('SKIPPING: No example supports requested platform(s).'), + ]), + ); + + expect(processRunner.recordedCalls.isEmpty, true); + }); + + test('drive only supported examples if there is more than one', () async { + final Directory package = createFakePackage('a_package', packagesDir, + isFlutter: true, + examples: [ + 'with_web', + 'without_web' + ], + extraFiles: [ + 'example/with_web/integration_test/foo_test.dart', + 'example/with_web/test_driver/integration_test.dart', + 'example/with_web/web/index.html', + 'example/without_web/integration_test/foo_test.dart', + 'example/without_web/test_driver/integration_test.dart', + ]); + final Directory supportedExampleDirectory = + package.childDirectory('example').childDirectory('with_web'); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package'), + contains( + 'Skipping a_package/example/without_web; does not support any requested platforms.'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + 'web-server', + '--web-port=7357', + '--browser-name=chrome', + '--driver', + 'test_driver/integration_test.dart', + '--target', + 'integration_test/foo_test.dart' + ], + supportedExampleDirectory.path), + ])); + }); + + test('are skipped when there is no integration testing', () async { + createFakePackage('a_package', packagesDir, + isFlutter: true, extraFiles: ['example/web/index.html']); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package'), + contains('SKIPPING: No example is configured for driver tests.'), + ]), + ); + + expect(processRunner.recordedCalls.isEmpty, true); + }); + }); }); } diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index 2890c528e4c1..6c10a7dc3209 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -448,7 +448,7 @@ void main() { ])); }); - test('fails if files are changed with --file-on-change', () async { + test('fails if files are changed with --fail-on-change', () async { const List files = [ 'linux/foo_plugin.cc', 'macos/Classes/Foo.h', diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index e97274afd09e..efaf969c83fb 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -7,24 +7,35 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/license_check_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; import 'package:test/test.dart'; +import 'common/plugin_command_test.mocks.dart'; +import 'mocks.dart'; import 'util.dart'; void main() { - group('$LicenseCheckCommand', () { + group('LicenseCheckCommand', () { late CommandRunner runner; late FileSystem fileSystem; + late Platform platform; late Directory root; setUp(() { fileSystem = MemoryFileSystem(); + platform = MockPlatformWithSeparator(); final Directory packagesDir = fileSystem.currentDirectory.childDirectory('packages'); root = packagesDir.parent; + final MockGitDir gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); + final LicenseCheckCommand command = LicenseCheckCommand( packagesDir, + platform: platform, + gitDir: gitDir, ); runner = CommandRunner('license_test', 'Test for $LicenseCheckCommand'); @@ -123,6 +134,33 @@ void main() { } }); + test('ignores submodules', () async { + const String submoduleName = 'a_submodule'; + + final File submoduleSpec = root.childFile('.gitmodules'); + submoduleSpec.writeAsStringSync(''' +[submodule "$submoduleName"] + path = $submoduleName + url = https://github.com/foo/$submoduleName +'''); + + const List submoduleFiles = [ + '$submoduleName/foo.dart', + '$submoduleName/a/b/bar.dart', + '$submoduleName/LICENSE', + ]; + for (final String filePath in submoduleFiles) { + root.childFile(filePath).createSync(recursive: true); + } + + final List output = + await runCapturingPrint(runner, ['license-check']); + + for (final String filePath in submoduleFiles) { + expect(output, isNot(contains('Checking $filePath'))); + } + }); + test('passes if all checked files have license blocks', () async { final File checked = root.childFile('checked.cc'); checked.createSync(); @@ -509,6 +547,11 @@ void main() { }); } +class MockPlatformWithSeparator extends MockPlatform { + @override + String get pathSeparator => isWindows ? r'\' : '/'; +} + const String _correctLicenseFileText = ''' Copyright 2013 The Flutter Authors. All rights reserved. diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index fcdf9fafdb63..9e70f72e7483 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -12,7 +12,7 @@ import 'mocks.dart'; import 'util.dart'; void main() { - group('$ListCommand', () { + group('ListCommand', () { late FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; diff --git a/script/tool/test/make_deps_path_based_command_test.dart b/script/tool/test/make_deps_path_based_command_test.dart index 2021f24079e3..da241c3d83f7 100644 --- a/script/tool/test/make_deps_path_based_command_test.dart +++ b/script/tool/test/make_deps_path_based_command_test.dart @@ -144,6 +144,52 @@ void main() { ])); }); + // This test case ensures that running CI using this command on an interim + // PR that itself used this command won't fail on the rewrite step. + test('running a second time no-ops without failing', () async { + final RepositoryPackage simplePackage = RepositoryPackage( + createFakePackage('foo', packagesDir, isFlutter: true)); + final Directory pluginGroup = packagesDir.childDirectory('bar'); + + RepositoryPackage(createFakePackage('bar_platform_interface', pluginGroup, + isFlutter: true)); + final RepositoryPackage pluginImplementation = + RepositoryPackage(createFakePlugin('bar_android', pluginGroup)); + final RepositoryPackage pluginAppFacing = + RepositoryPackage(createFakePlugin('bar', pluginGroup)); + + _addDependencies(simplePackage, [ + 'bar', + 'bar_android', + 'bar_platform_interface', + ]); + _addDependencies(pluginAppFacing, [ + 'bar_platform_interface', + 'bar_android', + ]); + _addDependencies(pluginImplementation, [ + 'bar_platform_interface', + ]); + + await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies=bar,bar_platform_interface' + ]); + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies=bar,bar_platform_interface' + ]); + + expect( + output, + containsAll([ + 'Rewriting references to: bar, bar_platform_interface...', + ' Skipped packages/bar/bar/pubspec.yaml - Already rewritten', + ' Skipped packages/bar/bar_android/pubspec.yaml - Already rewritten', + ' Skipped packages/foo/pubspec.yaml - Already rewritten', + ])); + }); + group('target-dependencies-with-non-breaking-updates', () { test('no-ops for no published changes', () async { final Directory package = createFakePackage('foo', packagesDir); diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 42d20240da87..30b6ab6004e7 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -155,6 +155,21 @@ ${_flutterSection(isPlugin: true)} ${_dependenciesSection()} ${_devDependenciesSection()} ${_falseSecretsSection()} +'''); + + pluginDirectory + .childDirectory('example') + .childFile('pubspec.yaml') + .writeAsStringSync(''' +${_headerSection( + 'plugin_example', + publishable: false, + includeRepository: false, + includeIssueTracker: false, + )} +${_environmentSection()} +${_dependenciesSection()} +${_flutterSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -172,15 +187,31 @@ ${_falseSecretsSection()} }); test('passes for a Flutter package following conventions', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory packageDirectory = + createFakePackage('a_package', packagesDir); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${_headerSection('plugin')} + packageDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${_headerSection('a_package')} ${_environmentSection()} ${_dependenciesSection()} ${_devDependenciesSection()} ${_flutterSection()} ${_falseSecretsSection()} +'''); + + packageDirectory + .childDirectory('example') + .childFile('pubspec.yaml') + .writeAsStringSync(''' +${_headerSection( + 'a_package', + publishable: false, + includeRepository: false, + includeIssueTracker: false, + )} +${_environmentSection()} +${_dependenciesSection()} +${_flutterSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -190,8 +221,8 @@ ${_falseSecretsSection()} expect( output, containsAllInOrder([ - contains('Running for plugin...'), - contains('Running for plugin/example...'), + contains('Running for a_package...'), + contains('Running for a_package/example...'), contains('No issues found!'), ]), ); @@ -221,7 +252,8 @@ ${_dependenciesSection()} }); test('fails when homepage is included', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, includeHomepage: true)} @@ -248,7 +280,8 @@ ${_devDependenciesSection()} }); test('fails when repository is missing', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, includeRepository: false)} @@ -274,7 +307,8 @@ ${_devDependenciesSection()} }); test('fails when homepage is given instead of repository', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} @@ -301,7 +335,8 @@ ${_devDependenciesSection()} }); test('fails when repository is incorrect', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, repositoryPackagesDirRelativePath: 'different_plugin')} @@ -327,7 +362,8 @@ ${_devDependenciesSection()} }); test('fails when issue tracker is missing', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, includeIssueTracker: false)} @@ -353,8 +389,9 @@ ${_devDependenciesSection()} }); test('fails when description is too short', () async { - final Directory pluginDirectory = - createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); + final Directory pluginDirectory = createFakePlugin( + 'a_plugin', packagesDir.childDirectory('a_plugin'), + examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, description: 'Too short')} @@ -383,7 +420,8 @@ ${_devDependenciesSection()} test( 'allows short descriptions for non-app-facing parts of federated plugins', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, description: 'Too short')} @@ -410,7 +448,8 @@ ${_devDependenciesSection()} }); test('fails when description is too long', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); const String description = 'This description is too long. It just goes ' 'on and on and on and on and on. pub.dev will down-score it because ' @@ -442,7 +481,8 @@ ${_devDependenciesSection()} }); test('fails when environment section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} @@ -469,7 +509,8 @@ ${_environmentSection()} }); test('fails when flutter section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} @@ -496,7 +537,8 @@ ${_devDependenciesSection()} }); test('fails when dependencies section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} @@ -550,7 +592,8 @@ ${_dependenciesSection()} }); test('fails when false_secrets section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} @@ -580,7 +623,8 @@ ${_devDependenciesSection()} test('fails when an implemenation package is missing "implements"', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), + examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin_a_foo', isPlugin: true)} @@ -608,7 +652,8 @@ ${_devDependenciesSection()} test('fails when an implemenation package has the wrong "implements"', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), + examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('plugin_a_foo', isPlugin: true)} @@ -636,7 +681,8 @@ ${_devDependenciesSection()} test('passes for a correct implemenation package', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), + examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection( @@ -663,8 +709,9 @@ ${_devDependenciesSection()} }); test('fails when a "default_package" looks incorrect', () async { - final Directory pluginDirectory = - createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); + final Directory pluginDirectory = createFakePlugin( + 'plugin_a', packagesDir.childDirectory('plugin_a'), + examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection( @@ -702,8 +749,9 @@ ${_devDependenciesSection()} test( 'fails when a "default_package" does not have a corresponding dependency', () async { - final Directory pluginDirectory = - createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); + final Directory pluginDirectory = createFakePlugin( + 'plugin_a', packagesDir.childDirectory('plugin_a'), + examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection( @@ -739,8 +787,9 @@ ${_devDependenciesSection()} }); test('passes for an app-facing package without "implements"', () async { - final Directory pluginDirectory = - createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); + final Directory pluginDirectory = createFakePlugin( + 'plugin_a', packagesDir.childDirectory('plugin_a'), + examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection( @@ -769,8 +818,8 @@ ${_devDependenciesSection()} test('passes for a platform interface package without "implements"', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin_a_platform_interface', - packagesDir.childDirectory('plugin_a')); + 'plugin_a_platform_interface', packagesDir.childDirectory('plugin_a'), + examples: []); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection( @@ -799,7 +848,8 @@ ${_devDependenciesSection()} test('validates some properties even for unpublished packages', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), + examples: []); // Environment section is in the wrong location. // Missing 'implements'. @@ -829,7 +879,8 @@ ${_environmentSection()} }); test('ignores some checks for unpublished packages', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, examples: []); // Missing metadata that is only useful for published packages, such as // repository and issue tracker. @@ -886,7 +937,7 @@ ${_devDependenciesSection()} test('repository check works', () async { final Directory packageDirectory = - createFakePackage('package', packagesDir); + createFakePackage('package', packagesDir, examples: []); packageDirectory.childFile('pubspec.yaml').writeAsStringSync(''' ${_headerSection('package')} diff --git a/script/tool/test/readme_check_command_test.dart b/script/tool/test/readme_check_command_test.dart new file mode 100644 index 000000000000..b6e016dccab4 --- /dev/null +++ b/script/tool/test/readme_check_command_test.dart @@ -0,0 +1,409 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/readme_check_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + late CommandRunner runner; + late RecordingProcessRunner processRunner; + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = fileSystem.currentDirectory.childDirectory('packages'); + createPackagesDirectory(parentDir: packagesDir.parent); + processRunner = RecordingProcessRunner(); + final ReadmeCheckCommand command = ReadmeCheckCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'readme_check_command', 'Test for readme_check_command'); + runner.addCommand(command); + }); + + test('fails when README is missing', () async { + createFakePackage('a_package', packagesDir); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Missing README.md'), + ]), + ); + }); + + group('plugin OS support', () { + test( + 'does not check support table for anything other than app-facing plugin packages', + () async { + const String federatedPluginName = 'a_federated_plugin'; + final Directory federatedDir = + packagesDir.childDirectory(federatedPluginName); + final List packageDirectories = [ + // A non-plugin package. + createFakePackage('a_package', packagesDir), + // Non-app-facing parts of a federated plugin. + createFakePlugin( + '${federatedPluginName}_platform_interface', federatedDir), + createFakePlugin('${federatedPluginName}_android', federatedDir), + ]; + + for (final Directory package in packageDirectories) { + package.childFile('README.md').writeAsStringSync(''' +A very useful package. +'''); + } + + final List output = await runCapturingPrint(runner, [ + 'readme-check', + ]); + + expect( + output, + containsAll([ + contains('Running for a_package...'), + contains('Running for a_federated_plugin_platform_interface...'), + contains('Running for a_federated_plugin_android...'), + contains('No issues found!'), + ]), + ); + }); + + test('fails when non-federated plugin is missing an OS support table', + () async { + final Directory pluginDir = createFakePlugin('a_plugin', packagesDir); + + pluginDir.childFile('README.md').writeAsStringSync(''' +A very useful plugin. +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No OS support table found'), + ]), + ); + }); + + test( + 'fails when app-facing part of a federated plugin is missing an OS support table', + () async { + final Directory pluginDir = + createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); + + pluginDir.childFile('README.md').writeAsStringSync(''' +A very useful plugin. +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No OS support table found'), + ]), + ); + }); + + test('fails the OS support table is missing the header', () async { + final Directory pluginDir = createFakePlugin('a_plugin', packagesDir); + + pluginDir.childFile('README.md').writeAsStringSync(''' +A very useful plugin. + +| **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] | +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('OS support table does not have the expected header format'), + ]), + ); + }); + + test('fails if the OS support table is missing a supported OS', () async { + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), + }, + ); + + pluginDir.childFile('README.md').writeAsStringSync(''' +A very useful plugin. + +| | Android | iOS | +|----------------|---------|----------| +| **Support** | SDK 21+ | iOS 10+* | +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains(' OS support table does not match supported platforms:\n' + ' Actual: android, ios, web\n' + ' Documented: android, ios'), + contains('Incorrect OS support table'), + ]), + ); + }); + + test('fails if the OS support table lists an extra OS', () async { + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), + }, + ); + + pluginDir.childFile('README.md').writeAsStringSync(''' +A very useful plugin. + +| | Android | iOS | Web | +|----------------|---------|----------|------------------------| +| **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] | +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains(' OS support table does not match supported platforms:\n' + ' Actual: android, ios\n' + ' Documented: android, ios, web'), + contains('Incorrect OS support table'), + ]), + ); + }); + + test('fails if the OS support table has unexpected OS formatting', + () async { + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), + }, + ); + + pluginDir.childFile('README.md').writeAsStringSync(''' +A very useful plugin. + +| | android | ios | MacOS | web | +|----------------|---------|----------|-------|------------------------| +| **Support** | SDK 21+ | iOS 10+* | 10.11 | [See `camera_web `][1] | +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains(' Incorrect OS capitalization: android, ios, MacOS, web\n' + ' Please use standard capitalizations: Android, iOS, macOS, Web\n'), + contains('Incorrect OS support formatting'), + ]), + ); + }); + }); + + group('code blocks', () { + test('fails on missing info string', () async { + final Directory packageDir = createFakePackage('a_package', packagesDir); + + packageDir.childFile('README.md').writeAsStringSync(''' +Example: + +``` +void main() { + // ... +} +``` +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Code block at line 3 is missing a language identifier.'), + contains('Missing language identifier for code block'), + ]), + ); + }); + + test('allows unknown info strings', () async { + final Directory packageDir = createFakePackage('a_package', packagesDir); + + packageDir.childFile('README.md').writeAsStringSync(''' +Example: + +```someunknowninfotag +A B C +``` +'''); + + final List output = await runCapturingPrint(runner, [ + 'readme-check', + ]); + + expect( + output, + containsAll([ + contains('Running for a_package...'), + contains('No issues found!'), + ]), + ); + }); + + test('allows space around info strings', () async { + final Directory packageDir = createFakePackage('a_package', packagesDir); + + packageDir.childFile('README.md').writeAsStringSync(''' +Example: + +``` dart +A B C +``` +'''); + + final List output = await runCapturingPrint(runner, [ + 'readme-check', + ]); + + expect( + output, + containsAll([ + contains('Running for a_package...'), + contains('No issues found!'), + ]), + ); + }); + + test('passes when excerpt requirement is met', () async { + final Directory packageDir = createFakePackage('a_package', packagesDir); + + packageDir.childFile('README.md').writeAsStringSync(''' +Example: + + +```dart +A B C +``` +'''); + + final List output = await runCapturingPrint( + runner, ['readme-check', '--require-excerpts']); + + expect( + output, + containsAll([ + contains('Running for a_package...'), + contains('No issues found!'), + ]), + ); + }); + + test('fails on missing excerpt tag when requested', () async { + final Directory packageDir = createFakePackage('a_package', packagesDir); + + packageDir.childFile('README.md').writeAsStringSync(''' +Example: + +```dart +A B C +``` +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['readme-check', '--require-excerpts'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Dart code block at line 3 is not managed by code-excerpt.'), + contains('Missing code-excerpt management for code block'), + ]), + ); + }); + }); +} diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 9bcd8d1ae67a..386eaf0d345b 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -58,6 +58,28 @@ void main() { ); }); + test('runs flutter test on Flutter package example tests', () async { + final Directory pluginDir = createFakePlugin('a_plugin', packagesDir, + extraFiles: [ + 'test/empty_test.dart', + 'example/test/an_example_test.dart' + ]); + + await runCapturingPrint(runner, ['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['test', '--color'], pluginDir.path), + ProcessCall( + getFlutterCommand(mockPlatform), + const ['test', '--color'], + pluginDir.childDirectory('example').path), + ]), + ); + }); + test('fails when Flutter tests fail', () async { createFakePlugin('plugin1', packagesDir, extraFiles: ['test/empty_test.dart']); @@ -102,7 +124,7 @@ void main() { ); }); - test('runs pub run test on non-Flutter packages', () async { + test('runs dart run test on non-Flutter packages', () async { final Directory pluginDir = createFakePlugin('a', packagesDir, extraFiles: ['test/empty_test.dart']); final Directory packageDir = createFakePackage('b', packagesDir, @@ -121,12 +143,34 @@ void main() { ProcessCall('dart', const ['pub', 'get'], packageDir.path), ProcessCall( 'dart', - const ['pub', 'run', '--enable-experiment=exp1', 'test'], + const ['run', '--enable-experiment=exp1', 'test'], packageDir.path), ]), ); }); + test('runs dart run test on non-Flutter package examples', () async { + final Directory packageDir = createFakePackage('a_package', packagesDir, + extraFiles: [ + 'test/empty_test.dart', + 'example/test/an_example_test.dart' + ]); + + await runCapturingPrint(runner, ['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('dart', const ['pub', 'get'], packageDir.path), + ProcessCall('dart', const ['run', 'test'], packageDir.path), + ProcessCall('dart', const ['pub', 'get'], + packageDir.childDirectory('example').path), + ProcessCall('dart', const ['run', 'test'], + packageDir.childDirectory('example').path), + ]), + ); + }); + test('fails when getting non-Flutter package dependencies fails', () async { createFakePackage('a_package', packagesDir, extraFiles: ['test/empty_test.dart']); @@ -217,7 +261,7 @@ void main() { ProcessCall('dart', const ['pub', 'get'], packageDir.path), ProcessCall( 'dart', - const ['pub', 'run', '--enable-experiment=exp1', 'test'], + const ['run', '--enable-experiment=exp1', 'test'], packageDir.path), ]), ); diff --git a/script/tool/test/update_excerpts_command_test.dart b/script/tool/test/update_excerpts_command_test.dart new file mode 100644 index 000000000000..30189cf23a00 --- /dev/null +++ b/script/tool/test/update_excerpts_command_test.dart @@ -0,0 +1,284 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; +import 'package:flutter_plugin_tools/src/update_excerpts_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'common/plugin_command_test.mocks.dart'; +import 'mocks.dart'; +import 'util.dart'; + +void main() { + late FileSystem fileSystem; + late Directory packagesDir; + late RecordingProcessRunner processRunner; + late CommandRunner runner; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + final MockGitDir gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); + processRunner = RecordingProcessRunner(); + final UpdateExcerptsCommand command = UpdateExcerptsCommand( + packagesDir, + processRunner: processRunner, + platform: MockPlatform(), + gitDir: gitDir, + ); + + runner = CommandRunner( + 'update_excerpts_command', 'Test for update_excerpts_command'); + runner.addCommand(command); + }); + + test('runs pub get before running scripts', () async { + final Directory package = createFakePlugin('a_package', packagesDir, + extraFiles: ['example/build.excerpt.yaml']); + final Directory example = package.childDirectory('example'); + + await runCapturingPrint(runner, ['update-excerpts']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall('dart', const ['pub', 'get'], example.path), + ProcessCall( + 'dart', + const [ + 'run', + 'build_runner', + 'build', + '--config', + 'excerpt', + '--output', + 'excerpts', + '--delete-conflicting-outputs', + ], + example.path), + ])); + }); + + test('runs when config is present', () async { + final Directory package = createFakePlugin('a_package', packagesDir, + extraFiles: ['example/build.excerpt.yaml']); + final Directory example = package.childDirectory('example'); + + final List output = + await runCapturingPrint(runner, ['update-excerpts']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall( + 'dart', + const [ + 'run', + 'build_runner', + 'build', + '--config', + 'excerpt', + '--output', + 'excerpts', + '--delete-conflicting-outputs', + ], + example.path), + ProcessCall( + 'dart', + const [ + 'run', + 'code_excerpt_updater', + '--write-in-place', + '--yaml', + '--no-escape-ng-interpolation', + '../README.md', + ], + example.path), + ])); + + expect( + output, + containsAllInOrder([ + contains('Ran for 1 package(s)'), + ])); + }); + + test('skips when no config is present', () async { + createFakePlugin('a_package', packagesDir); + + final List output = + await runCapturingPrint(runner, ['update-excerpts']); + + expect(processRunner.recordedCalls, isEmpty); + + expect( + output, + containsAllInOrder([ + contains('Skipped 1 package(s)'), + ])); + }); + + test('restores pubspec even if running the script fails', () async { + final Directory package = createFakePlugin('a_package', packagesDir, + extraFiles: ['example/build.excerpt.yaml']); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 1), // dart pub get + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['update-excerpts'], errorHandler: (Error e) { + commandError = e; + }); + + // Check that it's definitely a failure in a step between making the changes + // and restoring the original. + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package:\n' + ' Unable to get script dependencies') + ])); + + final String examplePubspecContent = RepositoryPackage(package) + .getExamples() + .first + .pubspecFile + .readAsStringSync(); + expect(examplePubspecContent, isNot(contains('code_excerpter'))); + expect(examplePubspecContent, isNot(contains('code_excerpt_updater'))); + }); + + test('fails if pub get fails', () async { + createFakePlugin('a_package', packagesDir, + extraFiles: ['example/build.excerpt.yaml']); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 1), // dart pub get + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['update-excerpts'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package:\n' + ' Unable to get script dependencies') + ])); + }); + + test('fails if extraction fails', () async { + createFakePlugin('a_package', packagesDir, + extraFiles: ['example/build.excerpt.yaml']); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 0), // dart pub get + MockProcess(exitCode: 1), // dart run build_runner ... + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['update-excerpts'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package:\n' + ' Unable to extract excerpts') + ])); + }); + + test('fails if injection fails', () async { + createFakePlugin('a_package', packagesDir, + extraFiles: ['example/build.excerpt.yaml']); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 0), // dart pub get + MockProcess(exitCode: 0), // dart run build_runner ... + MockProcess(exitCode: 1), // dart run code_excerpt_updater ... + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['update-excerpts'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package:\n' + ' Unable to inject excerpts') + ])); + }); + + test('fails if files are changed with --fail-on-change', () async { + createFakePlugin('a_plugin', packagesDir, + extraFiles: ['example/build.excerpt.yaml']); + + const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; + processRunner.mockProcessesForExecutable['git'] = [ + MockProcess(stdout: changedFilePath), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['update-excerpts', '--fail-on-change'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('README.md is out of sync with its source excerpts'), + ])); + }); + + test('fails if git ls-files fails', () async { + createFakePlugin('a_plugin', packagesDir, + extraFiles: ['example/build.excerpt.yaml']); + + processRunner.mockProcessesForExecutable['git'] = [ + MockProcess(exitCode: 1) + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['update-excerpts', '--fail-on-change'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to determine local file state'), + ])); + }); +} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 6f7d86e054e9..8a2bf099cc8a 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -47,7 +47,6 @@ Directory createPackagesDirectory( class PlatformDetails { const PlatformDetails( this.type, { - this.variants = const [], this.hasNativeCode = true, this.hasDartCode = false, }); @@ -55,9 +54,6 @@ class PlatformDetails { /// The type of support for the platform. final PlatformSupport type; - /// Any 'supportVariants' to list in the pubspec. - final List variants; - /// Whether or not the plugin includes native code. /// /// Ignored for web, which does not have native code. @@ -111,6 +107,12 @@ Directory createFakePlugin( /// /// [extraFiles] is an optional list of package-relative paths, using unix-style /// separators, of extra files to create in the package. +/// +/// If [includeCommonFiles] is true, common but non-critical files like +/// CHANGELOG.md and AUTHORS will be included. +/// +/// If non-null, [directoryName] will be used for the directory instead of +/// [name]. // TODO(stuartmorgan): Convert the return to a RepositoryPackage. Directory createFakePackage( String name, @@ -120,37 +122,43 @@ Directory createFakePackage( bool isFlutter = false, String? version = '0.0.1', String flutterConstraint = '>=2.5.0', + bool includeCommonFiles = true, + String? directoryName, + String? publishTo, }) { - final Directory packageDirectory = parentDirectory.childDirectory(name); + final Directory packageDirectory = + parentDirectory.childDirectory(directoryName ?? name); packageDirectory.createSync(recursive: true); + packageDirectory.childDirectory('lib').createSync(); createFakePubspec(packageDirectory, name: name, isFlutter: isFlutter, version: version, flutterConstraint: flutterConstraint); - createFakeCHANGELOG(packageDirectory, ''' + if (includeCommonFiles) { + createFakeCHANGELOG(packageDirectory, ''' ## $version * Some changes. '''); - createFakeAuthors(packageDirectory); + createFakeAuthors(packageDirectory); + } if (examples.length == 1) { - final Directory exampleDir = packageDirectory.childDirectory(examples.first) - ..createSync(); - createFakePubspec(exampleDir, - name: '${name}_example', + createFakePackage('${name}_example', packageDirectory, + directoryName: examples.first, + examples: [], + includeCommonFiles: false, isFlutter: isFlutter, publishTo: 'none', flutterConstraint: flutterConstraint); } else if (examples.isNotEmpty) { - final Directory exampleDir = packageDirectory.childDirectory('example') - ..createSync(); - for (final String example in examples) { - final Directory currentExample = exampleDir.childDirectory(example) - ..createSync(); - createFakePubspec(currentExample, - name: example, + final Directory examplesDirectory = + packageDirectory.childDirectory('example')..createSync(); + for (final String exampleName in examples) { + createFakePackage(exampleName, examplesDirectory, + examples: [], + includeCommonFiles: false, isFlutter: isFlutter, publishTo: 'none', flutterConstraint: flutterConstraint); @@ -183,7 +191,7 @@ void createFakePubspec( bool isPlugin = false, Map platformSupport = const {}, - String publishTo = 'http://no_pub_server.com', + String? publishTo, String? version, String dartConstraint = '>=2.0.0 <3.0.0', String flutterConstraint = '>=2.5.0', @@ -223,9 +231,16 @@ flutter: } } - String yaml = ''' + // Default to a fake server to avoid ever accidentally publishing something + // from a test. Does not use 'none' since that changes the behavior of some + // commands. + final String publishToSection = + 'publish_to: ${publishTo ?? 'http://no_pub_server.com'}'; + + final String yaml = ''' name: $name ${(version != null) ? 'version: $version' : ''} +$publishToSection $environmentSection @@ -234,11 +249,6 @@ $dependenciesSection $pluginSection '''; - if (publishTo.isNotEmpty) { - yaml += ''' -publish_to: $publishTo # Hardcoded safeguard to prevent this from somehow being published by a broken test. -'''; - } parent.childFile('pubspec.yaml').createSync(); parent.childFile('pubspec.yaml').writeAsStringSync(yaml); } @@ -293,18 +303,6 @@ String _pluginPlatformSection( entry = lines.join('\n') + '\n'; } - // Add any variants. - if (support.variants.isNotEmpty) { - entry += ''' - supportedVariants: -'''; - for (final String variant in support.variants) { - entry += ''' - - $variant -'''; - } - } - return entry; } @@ -434,8 +432,7 @@ class ProcessCall { } @override - int get hashCode => - (executable.hashCode) ^ (args.hashCode) ^ (workingDir?.hashCode ?? 0); + int get hashCode => Object.hash(executable, args, workingDir); @override String toString() { diff --git a/site-shared b/site-shared new file mode 160000 index 000000000000..142de133477b --- /dev/null +++ b/site-shared @@ -0,0 +1 @@ +Subproject commit 142de133477bdede1746f992e656c4b43c4c7442